從 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 。你只需選擇適合你的最佳系統。