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 context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ImageRemoveGridCell(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
_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(context, R.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 context, IView imageRemoveGridCell, IInitializer initializer) {
_initializer = initializer == null ? new Initializer(imageRemoveGridCell) : initializer;
_initializer.inflate(context);
_cell = _initializer.getLayoutFromGridCell();
}
public void setLayoutParams(ViewGroup.LayoutParams params) {
RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) _image.getLayoutParams();
p.height = params.height;
p.width = params.width;
_image.setLayoutParams(p);
}
public void addImage(ImageView image, View.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, null, mockInitializer);
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(_mockImage, dummyListener);
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(_mockImage, createNiceMock(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 <T extends ViewGroup.LayoutParams> T 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.LayoutParams) argument;
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