剑客
关注科技互联网

Android 单元测试实践

单元测试是什么

单元测试是针对 程序的最小单元
来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。一个单元可能是 单个程序、类、对象、方法
等。 ——维基百科

为什么要做单元测试

卖个关子,看完文章自然就知道了

原来和很多人一样并没有写单元测试的习惯,写好一个功能模块之后直接在真机上做自测,看看刚写的功能是否和预期一致,如果不一致,从头debug找问题出在哪儿,没问题就提交测试,测试测出问题,再从头debug找问题出在哪儿。这个过程一般会比较费时,但一直以来都这么干,也没发现有什么问题。

后来看到一些安利单元测试的文章,被洗脑似的决定开始写单元测试。

然后,就没有然后了。

原来所有的代码逻辑都在Activity里,如何写单元测试?瞬间懵逼了。

很多公司或个人不愿意写单元测试的原因可能是觉得写单元测试并没用有什么卵用,项目比较赶根本没有时间写单元测试,不知道从何下手,特别是 Android 应用更是比较难写单元测试。

后来看到有文章说使用MVP模式可以方便的写单元测试,而且可以使用Junit写单元测试,直接运行在JVM上,而不需要运行在Android环境中。

然后就有了这篇文章 《如何将原项目重构成MVP模式》

开始实践

写了这么多铺垫,终于可以开始操刀写单元测试了,各位看官是不是已经急不可耐了。

就拿这个类开始写单元测试吧:

public class CreditCardPresenter extends BasePresenter<CreditCardContract.View, CreditCardContract.Model> implements CreditCardContract.Presenter {
    
    //其他代码略
    public void getCreditCards() {
        getModel().getCreditCards()
                .subscribe(new Subscriber<List<CreditCard>>(){
                    @Overridec
                    public void onNext(List<CreditCard> creditCards) {
                        getView().showCreditCards(creditCards);
                    }
                    
                    @Override
                    public void onCompleted() {
                        getView().loadCompleted();
                    }
                    
                    @Override
                    public void onError(Throwable e) {
                        getView().showError(e);
                    }
                    
                });
    }
    
}

功能很简单,就是获取信用卡列表,如果获取成功就通过下面的代码显示:

getView().showCreditCards(creditCards);
getView().loadCompleted();

如果出错,则通知页面显示错误信息:

getView().showError(e);

又懵逼了,getModel() 和 getView() 里面还是会调用安卓的代码,怎么使用Junit做测试呢?

引入一个强大的测试框架:Mockito,接下来就可以开始使用Junit & Mockito做Java代码的单元测试了,这种方式的单元测试可以直接运行与JVM上,使用Mockito隔离Android相关代码。

然后就可以为CreditCardPresenter 写单元测试了, 为了方便,静态导入了Mockito的所有方法

import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class CreditCardPresenterTest {

    CreditCardPresenter creditCardPresenter;
    @Mock
    CreditCardContract.View creditCardView;
    @Mock
    CreditCardContract.Model creditCardModel;

    List<CreditCard> creditCards;

    @Before
    public void setUp() throws Exception {
        creditCardPresenter = new CreditCardPresenter();
        creditCardPresenter.attachView(creditCardView);
        creditCardPresenter.setModel(creditCardModel);
        creditCards = new ArrayList<>();
    }

    public void testGetCreditCards() {
        when(creditCardModel.getCreditCards()).thenReturn(Observable.create(new Observable.OnSubscribe<List<CreditCard>>() {
            @Override
            public void call(Subscriber<? super List<CreditCard>> subscriber) {
                subscriber.onNext(creditCards);
                subscriber.onCompleted();
            }
        }));

        creditCardPresenter.getCreditCards();

        verify(creditCardView).showCreditCards(creditCards);
        verify(creditCardView).loadCompleted();
    }

    public void testGetCreditCardsOnError() {
        final RuntimeException exception = new RuntimeException();
        when(creditCardModel.getCreditCards()).thenReturn(Observable.create(new Observable.OnSubscribe<List<CreditCard>>() {
            @Override
            public void call(Subscriber<? super List<CreditCard>> subscriber) {
                throw exception;
            }
        }));

        creditCardPresenter.getCreditCards();

        verify(creditCardView).showError(exception);
    }

}

这样就为上述两种情况写了两个单元测试。

其中使用@Mock注解来生成mock对象,也可以setUp方法中使用Mockito.mock()来生成mock对象,当使用注解的时候在类上必须加上注解@RunWith(MockitoJUnitRunner.class)

mock出来的对象的方法都是空实现,void方法声明也不做,有返回值的方法返回null(int 类型返回0,boolean类型返回false等)。

然后我们可以通过when(…).thenReturn(…)来为mock对象实现方法返回值。

when(creditCardModel.getCreditCards()).thenReturn(Observable.create(new Observable.OnSubscribe<List<CreditCard>>() {
            @Override
            public void call(Subscriber<? super List<CreditCard>> subscriber) {
                subscriber.onNext(creditCards);
                subscriber.onCompleted();
            }
        }));

上面代码的意思就是说当调用creditCardModel.getCreditCards()的时候返回值是:

Observable.create(new Observable.OnSubscribe<List<CreditCard>>() {
            @Override
            public void call(Subscriber<? super List<CreditCard>> subscriber) {
                subscriber.onNext(creditCards);
                subscriber.onCompleted();
            }
        })

最后使用verify()方法来校验某个方法是否被执行:

verify(creditCardView).showCreditCards(creditCards);

上面的代码意思就是说 creditCardView.showCreditCards(creditCards)方法被执行了,并且参数是creditCards,并且只执行了一次。如果有一个条件不符合就会报测试失败。

verify()还有很多重载方法,默认其实是这样的 verfy(creditCardView, times(1)).showCreditCards(creditCards); 校验只执行了一次,times(1) 可以传入不同的参数来校验方法被执行了几次。还可以替换了nerver(),表示某方法一次也不执行。

当然Mockito的功能远不止这么点,还有很多高级用法就不继续介绍了。

Mockito也有一些美中不足之处,不能mock静态方法,final方法等,比如项目中会有这样的方法 SelfApplication.getContext() 来获取自定义的Application,如果在测试代码中出现这类代码肯定会测试失败,因为JVM环境中没有Application,怎么办呢?

再引入一个配合Mockito使用的库:PowerMock

他弥补了Mockito的不足,可以mock静态方法和final方法,可以使用PowerMock来mock出SelfApplication.getContext(),从而不会调用到真正的Application对象:

PowerMockito.mockStatic(SelfApplication.class);
PowerMockito.when(SelfApplication.getContext()).thenReturn(mock(SelfApplication.class));

另外,在方法上要声明@PrepareForTest(SelfApplication.class), 在类上要声明  @RunWith(PowerMockRunner.class) 来支持上述mock。

这样当 调用SelfApplication.getContext()的时候将拿到一个mock对象,我们就可以继续使用when().thenReturn()方法来处理方法返回值了。

具体关于Mockito 和 PowerMock 的更多用法这里就不做过多介绍了,官网才是最好的教程。

这只是一个简单的例子,实际项目中会出现好的复杂的情况。这就要求我写的代码方法要短,耦合要低。写单元测试逼迫我们写更优雅的代码,也为我们下次修改需求或者重构代码提供了一道安全保障。还有其他更多的好处大家自己在实践中体会吧。

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址