Tuesday, February 12, 2013

Unit testing a custom Android view

Introduction:

Unit testing a custom View in Android can be accomplished without having to run the tests in an emulator or having to rely on a framework such as Robolectric. The trick is to structure the source code to avoid View constructors that would lead to unit test failures. This example makes use of JUnit and EasyMock. It demonstrates unit testing of a custom view that overlays two ImageView elements inside of a RelativeLayout. The top image is hard-coded to be a small symbol. The bottom image is inserted in code.

 

Layout:

<?xmlversion="1.0"encoding="utf-8"?>

<mergexmlns:android="http://schemas.android.com/apk/res/android">

<RelativeLayout

  android:id="@+id/image_with_remove_cell"

  android:layout_width="wrap_content"

  android:layout_height="wrap_content">

    

    <!-- bottom ImageView is inserted here using code -->

 

    <ImageView

      android:id="@+id/remove_image_icon"

      android:layout_width="wrap_content"

      android:layout_height="wrap_content"

      android:src="@drawable/delete"

      android:layout_alignParentTop="true"

      android:layout_alignParentRight="true"

      android:contentDescription="@string/delete_thumbnail_contentDescription"/>

    

</RelativeLayout>

</merge>

 

Source Code:

The source code is structured so that the functionality can be unit tested without having to instantiate the ImageGridView class.  Subtypes are employed to reduce the number of class files required.

package com.crdn.android.app;

 

import com.gravityworksdesign.util.IView;

 

import android.content.Context;

import android.util.AttributeSet;

import android.view.View;

import android.view.ViewGroup;

import android.widget.ImageView;

import android.widget.RelativeLayout;

 

public class ImageRemoveGridCell extends RelativeLayout implements IView {

 

    private final ImageRemoveGridCellUtil _util;

    

    public ImageRemoveGridCell(Context context) {

        this(context, null);

    }

 

    public ImageRemoveGridCell(Context contextAttributeSet attrs) {

        this(contextattrs, 0);

    }

 

    public ImageRemoveGridCell(Context contextAttributeSet attrs, int defStyle) {

        super(contextattrsdefStyle);

        

        _util = new ImageRemoveGridCellUtil(context, this, null);

    }

    

    public ImageRemoveGridCellUtil getUtil() {

        return _util;

    }

    

    

    public interface IInitializer {

        void inflate(Context context);

        RelativeLayout getLayoutFromGridCell();

    }

    

    public class Initializer implements IInitializer {

        

        private final IView _view;

        

        public Initializer(IView view) {

            _view view;

        }

 

        @Override

        public void inflate(Context context) {

            View.inflate(contextR.layout.widget_image_with_remove_cell, (ViewGroup_view);

        }

 

        @Override

        public RelativeLayout getLayoutFromGridCell() {

            return (RelativeLayout_view.findViewById(R.id.image_with_remove_cell);

        }

        

    }

    

    public class ImageRemoveGridCellUtil {

        private final IInitializer _initializer;

        private final RelativeLayout _cell;

        private ImageView _image;

        

        public ImageRemoveGridCellUtil(Context contextIView imageRemoveGridCellIInitializer initializer) {

            

            _initializer initializer == null ? new Initializer(imageRemoveGridCell) initializer;

            _initializer.inflate(context);

            _cell _initializer.getLayoutFromGridCell();

        }

        

        public void setLayoutParams(ViewGroup.LayoutParams params) {

            

            RelativeLayout.LayoutParams = (RelativeLayout.LayoutParams_image.getLayoutParams();

            p.height params.height;

            p.width params.width;

            _image.setLayoutParams(p);

        }

        

        public void addImage(ImageView imageView.OnClickListener imageOnClickListener)  

            

            setImage(image);

            _image.setOnClickListener(imageOnClickListener);

            _cell.addView(_image, 0);

        }

 

        public ImageView getImage() {

 

            return _image;

        }

        

        public void setImage(ImageView image) {

            _image image;

        }

    }

}

 

Unit Test Code:

package com.crdn.android.app.test;

 

import static org.junit.Assert.*;

import static org.easymock.EasyMock.*;

 

import org.easymock.EasyMock;

import org.easymock.IArgumentMatcher;

import org.junit.After;

import org.junit.AfterClass;

import org.junit.Before;

import org.junit.BeforeClass;

import org.junit.Test;

 

import com.crdn.android.app.ImageRemoveGridCell;

 

import android.content.Context;

import android.view.View;

import android.view.ViewGroup;

import android.widget.ImageView;

import android.widget.RelativeLayout;

 

public class ImageRemoveGridCellTests {

 

    private static ImageRemoveGridCell.IInitializer _stubInitializer;

    private static RelativeLayout _mockCell;

    private static ImageView _mockImage;

    private ImageRemoveGridCell.ImageRemoveGridCellUtil _obj;

    

    @BeforeClass

    public static void setUpBeforeClass() throws Exception {

        _stubInitializer createNiceMock(ImageRemoveGridCell.IInitializer.class);

        _mockCell createStrictMock(RelativeLayout.class);

        _mockImage createStrictMock(ImageView.class);

    }

 

    @AfterClass

    public static void tearDownAfterClass() throws Exception {

    }

 

    @Before

    public void setUp() throws Exception {

        reset(_stubInitializer);

        reset(_mockCell);

        reset(_mockImage);

        

        Context dummyContext createNiceMock(Context.class);

        expect(_stubInitializer.getLayoutFromGridCell()).andReturn(_mockCell);      

        replay(_stubInitializer);

        

        _obj createNiceMock(ImageRemoveGridCell.class)

                .new ImageRemoveGridCellUtil(dummyContext, null_stubInitializer);

    }

 

    @After

    public void tearDown() throws Exception {

    }

    

    @Test

    public void construct_inflate_before_getLayoutFromGridCell() {

        

        Context dummyContext createNiceMock(Context.class);

        ImageRemoveGridCell.IInitializer mockInitializer createStrictMock(ImageRemoveGridCell.IInitializer.class);

        mockInitializer.inflate(dummyContext);

        expectLastCall().times(1);

        expect(mockInitializer.getLayoutFromGridCell()).andReturn(_mockCell);

        

        replay(mockInitializer);

        

        createNiceMock(ImageRemoveGridCell.class)

                .new ImageRemoveGridCellUtil(dummyContext, nullmockInitializer);

        

        verify(mockInitializer);

    }

 

    @Test

    public void addImage_sets_image_onClickListener_adds_image_view_to_cell() {

        

        View.OnClickListener dummyListener createNiceMock(View.OnClickListener.class);

        _mockImage.setOnClickListener(dummyListener);

        expectLastCall().times(1);

        

        _mockCell.addView(_mockImage, 0);

        expectLastCall().times(1);  

        

        replay(_mockImage);

        replay(_mockCell);

        

        _obj.addImage(_mockImagedummyListener);

        

        verify(_mockImage);

        verify(_mockCell);

    }

 

    @Test

    public void getImage_image_not_set_returns_null() {

        assertNull(_obj.getImage());

    }

    

    @Test

    public void getImage_after_addImage_returns_image() {

        

        _obj.addImage(_mockImagecreateNiceMock(View.OnClickListener.class));

        assertEquals(_mockImage_obj.getImage());

    }

    

    @Test

    public void setLayoutParams_transfers_height_and_width_from_provided_LayoutParams_to_image() {

        

        ViewGroup.LayoutParams stubParams createNiceMock(ViewGroup.LayoutParams.class);

        stubParams.height = 30;

        stubParams.width = 20;

        

        RelativeLayout.LayoutParams actualP createNiceMock(RelativeLayout.LayoutParams.class);

        

        expect(_mockImage.getLayoutParams()).andReturn(actualP);

        _mockImage.setLayoutParams(eqLayoutParams(stubParams));

        expectLastCall().times(1);

        replay(_mockImage);

        

        _obj.setImage(_mockImage);

        _obj.setLayoutParams(stubParams);

        

        verify(_mockImage);

    }

    

    public static <extends ViewGroup.LayoutParamsT eqLayoutParams(T in) {

        EasyMock.reportMatcher(new ImageRemoveGridCellTests().new ParamsEquals(in));

        return null;

    }

 

    public class ParamsEquals implements IArgumentMatcher {

    

        private ViewGroup.LayoutParams _expected;

        

        public ParamsEquals(ViewGroup.LayoutParams expected) {

            _expected expected;

        }

        

        @Override

        public boolean matches(Object argument) {

            if (argument == null || !(argument instanceof RelativeLayout.LayoutParams))

                return false;

            RelativeLayout.LayoutParams actual = (RelativeLayout.LayoutParamsargument;

            return actual.height == _expected.height && actual.width == _expected.width;

        }

    

        @Override

        public void appendTo(StringBuffer buffer) {

               buffer.append("eqLayoutParams(");

                buffer.append(_expected.getClass().getName());

                buffer.append(" with height '");

                buffer.append(_expected.height);

                buffer.append("' and with width '");

                buffer.append(_expected.width);

                buffer.append("'");

        }

    }

}

No comments:

Post a Comment