从 Android Componenets 中移出业务逻辑

本地 JVM 单元测试的许多价值来自你设计应用程序的方式。你必须以可以将业务逻辑与 Android 组件分离的方式进行设计。以下是使用 Model-View-Presenter 模式的这种方式的示例。让我们通过实现一个只需要用户名和密码的基本注册屏幕来实现这一点。我们的 Android 应用程序负责验证用户提供的用户名不是空白,密码长度至少为八个字符且至少包含一位数字。如果用户名/密码有效,我们会执行注册 api 调用,否则会显示错误消息。

业务逻辑与 Android 组件高度耦合的示例

public class LoginActivity extends Activity{
    ...
    private void onSubmitButtonClicked(){
        String username = findViewById(R.id.username).getText().toString();
        String password = findViewById(R.id.password).getText().toString();
        boolean isUsernameValid = username != null && username.trim().length() != 0;
        boolean isPasswordValid = password != null && password.trim().length() >= 8 && password.matches(".*\\d+.*");
        if(isUsernameValid && isPasswordValid){
            performSignUpApiCall(username, password);
        } else {
            displayInvalidCredentialsErrorMessage();
        }
    }
}

业务逻辑与 Android 组件分离的示例

在这里,我们在单个类中定义 LoginContract,它将容纳我们各个类之间的各种交互。

public interface LoginContract {
    public interface View {
        performSignUpApiCall(String username, String password);
        displayInvalidCredentialsErrorMessage();
    }
    public interface Presenter {
        void validateUserCredentials(String username, String password);
    }
}

除了我们已经不再需要知道如何验证用户的注册表单(我们的业务逻辑)之外,我们的 LoginActivity 大部分都是相同的。LoginActivity 现在将依赖我们的新 LoginPresenter 来执行验证。

public class LoginActivity extends Activity implements LoginContract.View{
    private LoginContract.Presenter presenter;

    protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            presenter = new LoginPresenter(this);
            ....
        }
        ...

        private void onSubmitButtonClicked(){
            String username = findViewById(R.id.username).getText().toString();
            String password = findViewById(R.id.password).getText().toString();
            presenter.validateUserCredentials(username, password);
    }
    ...
}

现在,你的业务逻辑将驻留在新的 LoginPresenter 类中。

public class LoginPresenter implements LoginContract.Presenter{
    private LoginContract.View view;

    public LoginPresenter(LoginContract.View view){
        this.view = view;
    }

    public void validateUserCredentials(String username, String password){
        boolean isUsernameValid = username != null && username.trim().length() != 0;
        boolean isPasswordValid = password != null && password.trim().length() >= 8 && password.matches(".*\\d+.*");
        if(isUsernameValid && isPasswordValid){
            view.performSignUpApiCall(username, password);
        } else {
            view.displayInvalidCredentialsErrorMessage();
        }
    }
}

现在我们可以针对你的新 LoginPresenter 类创建本地 JVM 单元测试。

public class LoginPresenterTest {

    @Mock
    LoginContract.View view;

    private LoginPresenter presenter;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        presenter = new LoginPresenter(view);
    }

    @Test
    public void test_validateUserCredentials_userDidNotEnterUsername_displayErrorMessage() throws Exception {
        String username = "";
        String password = "kingslayer1";
        presenter.validateUserCredentials(username, password);
        Mockito.verify(view). displayInvalidCredentialsErrorMessage();
    }

    @Test
    public void test_validateUserCredentials_userEnteredFourLettersAndOneDigitPassword_displayErrorMessage() throws Exception {
        String username = "Jaime Lanninster";
        String password = "king1";
        presenter.validateUserCredentials(username, password);
        Mockito.verify(view). displayInvalidCredentialsErrorMessage();
    }

    @Test
    public void test_validateUserCredentials_userEnteredNineLettersButNoDigitsPassword_displayErrorMessage() throws Exception {
        String username = "Jaime Lanninster";
        String password = "kingslayer";
        presenter.validateUserCredentials(username, password);
        Mockito.verify(view). displayInvalidCredentialsErrorMessage();
    }

    @Test
    public void test_validateUserCredentials_userEnteredNineLettersButOneDigitPassword_performApiCallToSignUpUser() throws Exception {
        String username = "Jaime Lanninster";
        String password = "kingslayer1";
        presenter.validateUserCredentials(username, password);
        Mockito.verify(view).performSignUpApiCall(username, password);
    }
}

如你所见,当我们从 LoginActivity 中提取业务逻辑并将其放入 LoginPresenter POJO 时 。我们现在可以根据业务逻辑创建本地 JVM 单元测试。

应该注意的是,我们的架构变化还有其他各种含义,例如我们接近每个类都有一个责任,额外的类等等。这些只是我选择执行此操作的方式的副作用通过 MVP 风格脱钩。MVP 只是解决这个问题的一种方法,但是你可能还想看看其他替代方案,比如 MVVM 。你只需选择适合你的最佳系统。