fred-ye / summary

my blog
43 stars 9 forks source link

[Android] Android中的单元测试-MVP, JUnit & Mockito #51

Open fred-ye opened 8 years ago

fred-ye commented 8 years ago

在此记录一下Android中如何采用JUnit和Macktio进行单元测试。 写代码总觉得有了测试心里面会更有底气一些,同时单元测试也会使我们的生产率大大提高。每完成一个功能就没有必要去拿到Android手机上去跑一下,这样太费时了。最近行业内对MVx模式炒得很热,昨晚在Youtube上看了个相关的视屏,采用MVP模式来架构一个App, 然后写单元测试很是方便,在此记录一下。代码是边看视屏边敲的,后来发现,其实作者已经将代码开源了。可以从这里找到。

关于Mockito

Google在其官方文档Building Local Unit Tests中提到,如果你的单元测试没有作何依赖或者仅仅依赖于Android, 你就应该在本地机器上跑你的测试case, 不应该让程序跑在移动设备或者模拟器中,这样是最高效,这个时候你需要用到一个Mocking framework, 像Mockito。 之前我们有用到Robolectric,感觉也还不错,不过既然Google推荐用Mockito,便来尝试一下。

默认情况下,我们的测试代码会方在src/test/java下,这是官方给规定的。

添加 Mockito 在build.gradle文件中

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'
    // required if you want to use Mockito for unit tests
    testCompile 'org.mockito:mockito-core:1.+'
    // required if you want to use Mockito for Android instrumentation tests
//    androidTestCompile 'org.mockito:mockito-core:1.+'
//    androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
//    androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
//    androidTestCompile 'junit:junit:4.12'

    compile 'com.android.support:appcompat-v7:23.1.1' //可删除
    compile 'com.android.support:design:23.1.1' //可删除
}

MVP

这篇文章里面讲得不错。个人觉得在Web的开发中,MVC是被大家所熟知,也很好理解。Model, View, Controller职责非常清楚。以普通的Java Web项目为例,通常会是一个JavaBean作为Model, 以JSP文件作为View, 以Servlet作为Controller,结构非常清晰。但在Android开发中,MVC架构是不能直接拿过来用的,主要因为Activity的角色不清晰,一方便要做为View, 另一方面要做为Controller。MVP的出现,则是分解Activity的功能,将其只做为一个View。

个人觉得MVP最大的缺点就是导致代码量太大了。

Demo中的MVP

public interface LoginView {
    String getUserName();
    String getPassword();
    void showUserNameError(int resId);
    void showPasswordError(int resId);
    void startMainActivity();
    void showLoginError(int resId);
}
public class LoginActivity extends Activity implements  LoginView {

    private LoginPresenter presenter;
    private EditText etUserName;
    private EditText etPassword;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        presenter = new LoginPresenter(this, new LoginService());
        initView();

    }

    private void initView() {
        etUserName = (EditText)findViewById(R.id.et_username);
        etPassword = (EditText) findViewById(R.id.et_password);
    }

    public void onLoginClicked(View view) {
        presenter.onLoginClicked();
    }

    @Override
    public String getUserName() {
        return etUserName.getText().toString();
    }

    @Override
    public String getPassword() {
        return etUserName.getText().toString();
    }

    @Override
    public void showUserNameError(int resId) {
        etUserName.setError(getString(resId));

    }

    @Override
    public void showPasswordError(int resId) {
        etPassword.setError(getString(resId));
    }

    @Override
    public void startMainActivity() {
        startActivity(new Intent(this, MainActivity.class));
    }

    @Override
    public void showLoginError(int resId) {
        Toast.makeText(this, resId, Toast.LENGTH_SHORT).show();
    }
}
public class LoginService {
    public boolean login(String username, String password) {
        return "james".equals(username) && "bond".equals(password);
    }
}
public class LoginPresenter {
    private LoginView view;
    private LoginService service;

    public LoginPresenter(LoginView view, LoginService service) {
        this.view = view;
        this.service = service;
    }

    public void onLoginClicked() {
        String userName = view.getUserName();
        if (userName.isEmpty()) {
            view.showUserNameError(R.string.username_error);
            return;
        }
        String password = view.getPassword();
        if (password.isEmpty()) {
            view.showPasswordError(R.string.password_error);
            return;
        }

        boolean flag = service.login(userName, password);
        if (flag) {
            view.startMainActivity();
            return;
        }
        view.showLoginError(R.string.login_failed);
    }
}
@RunWith(MockitoJUnitRunner.class)
public class LoginPresenterTest {
    @Mock
    private LoginView view;
    @Mock
    private LoginService service;

    private LoginPresenter presenter;
    @Before
    public void setUp() throws Exception {
        presenter = new LoginPresenter(view, service);
    }
    @Test
    public void showErrorMessageWhenUserNameIsEmpty() throws Exception {
        when(view.getUserName()).thenReturn("");
        presenter.onLoginClicked();
        verify(view).showUserNameError(R.string.username_error);
    }
    @Test
    public void showErrorMessageWhenPasswordIsEmpty() throws Exception {
        when(view.getUserName()).thenReturn("James");
        when(view.getPassword()).thenReturn("");
        presenter.onLoginClicked();
        verify(view).showPasswordError(R.string.password_error);
    }

    @Test
    public void startActivity() throws Exception {
        when(service.login("james", "bond")).thenReturn(true);
        when(view.getUserName()).thenReturn("james");
        when(view.getPassword()).thenReturn("bond");
        presenter.onLoginClicked();
        verify(view).startMainActivity();

    }

}

执行Case

Google是这么说的:

To run local unit tests in your Gradle project from Android Studio:

1. In the Project window, right click on the project and synchronize your project.
2. Open the Build Variants window by clicking the left-hand tab, then change the test artifact to Unit Tests.
3. In the Project window, drill down to your unit test class or method, then right-click and run it.
Android Studio displays the results of the unit test execution in the Run window.

贴个图就清楚了

1

再来看Mockito的基本使用

可以参看官方文档,这里只简单介绍一下。 采用Mockito来mock一个对象时, 既可以像上面的例子写的那样采用注解的方式,也可以用Mockito中的方法。 如:

Mock一个接口

import static org.mockito.Mockito.mock;

...

List mockedList = mock(List.class);

Mock 一个具体对象

 LinkedList mockedList = mock(LinkedList.class);

设置方法的预期返回值

when(view.getUserName()).thenReturn("James");

此时如果调用view.getUserName()返回的将会是"James". 当然还可以指定异常,如(这个例子来自官方):


 //You can mock concrete classes, not only interfaces
 LinkedList mockedList = mock(LinkedList.class);

 //stubbing
 when(mockedList.get(0)).thenReturn("first");
 when(mockedList.get(1)).thenThrow(new RuntimeException());

 //following prints "first"
 System.out.println(mockedList.get(0));

 //following throws runtime exception
 System.out.println(mockedList.get(1));

 //following prints "null" because get(999) was not stubbed
 System.out.println(mockedList.get(999));

验证方法是否被调用

如上面例子中验证startMainActivity 这个方法是否被调用

verify(view).startMainActivity();

验证超时时间

verify(mock, timeout(100)).someMethod();