From 347673c45384a8b71d1fadb54c4fcdf43c7191d0 Mon Sep 17 00:00:00 2001 From: Dariusz Bacinski Date: Fri, 3 Nov 2017 21:07:27 +0100 Subject: [PATCH 1/4] Migrated project to Kotlin, solved issues: - Java protected (package-private + inheritance) != Koltin protected (private + inheritance) - there is no package private scope to hide classes inside of package and expose just a public interface - removed logic and tests that verifies nulls - nullable view has to be guarded with .?, because smart cast does't work for var-s - builders replaced with data classes - Mockito fails on closed classes, fixed by Mock Maker - Mockito any() doesn't work in Kotlin, used alternative from mockito-kotlin - when is a keyword in Kotlin - replaced annotationProcessor with kapt --- app/build.gradle | 5 +- .../example/unittesting/BasePresenter.java | 29 ----- .../com/example/unittesting/BasePresenter.kt | 23 ++++ .../com/example/unittesting/Presenter.java | 8 -- .../com/example/unittesting/Presenter.kt | 8 ++ .../example/unittesting/ResourceProvider.java | 17 --- .../example/unittesting/ResourceProvider.kt | 11 ++ .../unittesting/SchedulersFactory.java | 21 ---- .../example/unittesting/SchedulersFactory.kt | 20 +++ .../login/model/LoginCredentials.java | 17 --- .../login/model/LoginCredentials.kt | 3 + .../login/model/LoginRepository.java | 20 --- .../login/model/LoginRepository.kt | 21 ++++ .../unittesting/login/model/LoginUseCase.java | 23 ---- .../unittesting/login/model/LoginUseCase.kt | 10 ++ .../login/model/LoginValidator.java | 15 --- .../unittesting/login/model/LoginValidator.kt | 17 +++ .../login/presenter/LoginPresenter.java | 78 ------------ .../login/presenter/LoginPresenter.kt | 57 +++++++++ .../login/presenter/LoginView.java | 18 --- .../unittesting/login/presenter/LoginView.kt | 18 +++ .../unittesting/login/view/LoginActivity.java | 117 ------------------ .../unittesting/login/view/LoginActivity.kt | 109 ++++++++++++++++ .../example/unittesting/BasePresenterTest.kt | 2 +- .../login/model/LoginRepositoryTest.kt | 4 +- .../login/model/LoginUseCaseTest.kt | 21 ---- .../login/model/LoginValidatorTest.kt | 17 --- .../login/presenter/LoginPresenterTest.kt | 13 +- .../org.mockito.plugins.MockMaker | 1 + 29 files changed, 311 insertions(+), 412 deletions(-) delete mode 100644 app/src/main/kotlin/com/example/unittesting/BasePresenter.java create mode 100644 app/src/main/kotlin/com/example/unittesting/BasePresenter.kt delete mode 100644 app/src/main/kotlin/com/example/unittesting/Presenter.java create mode 100644 app/src/main/kotlin/com/example/unittesting/Presenter.kt delete mode 100644 app/src/main/kotlin/com/example/unittesting/ResourceProvider.java create mode 100644 app/src/main/kotlin/com/example/unittesting/ResourceProvider.kt delete mode 100644 app/src/main/kotlin/com/example/unittesting/SchedulersFactory.java create mode 100644 app/src/main/kotlin/com/example/unittesting/SchedulersFactory.kt delete mode 100644 app/src/main/kotlin/com/example/unittesting/login/model/LoginCredentials.java create mode 100644 app/src/main/kotlin/com/example/unittesting/login/model/LoginCredentials.kt delete mode 100644 app/src/main/kotlin/com/example/unittesting/login/model/LoginRepository.java create mode 100644 app/src/main/kotlin/com/example/unittesting/login/model/LoginRepository.kt delete mode 100644 app/src/main/kotlin/com/example/unittesting/login/model/LoginUseCase.java create mode 100644 app/src/main/kotlin/com/example/unittesting/login/model/LoginUseCase.kt delete mode 100644 app/src/main/kotlin/com/example/unittesting/login/model/LoginValidator.java create mode 100644 app/src/main/kotlin/com/example/unittesting/login/model/LoginValidator.kt delete mode 100644 app/src/main/kotlin/com/example/unittesting/login/presenter/LoginPresenter.java create mode 100644 app/src/main/kotlin/com/example/unittesting/login/presenter/LoginPresenter.kt delete mode 100644 app/src/main/kotlin/com/example/unittesting/login/presenter/LoginView.java create mode 100644 app/src/main/kotlin/com/example/unittesting/login/presenter/LoginView.kt delete mode 100644 app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.java create mode 100644 app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.kt delete mode 100644 app/src/test/kotlin/com/example/unittesting/login/model/LoginUseCaseTest.kt create mode 100644 app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/app/build.gradle b/app/build.gradle index 2cf835a..117bed3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,13 +44,14 @@ dependencies { compile 'io.reactivex.rxjava2:rxandroid:2.0.0' compile 'com.jakewharton.timber:timber:4.3.0' compile 'com.jakewharton:butterknife:8.4.0' - annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0' + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + kapt 'com.jakewharton:butterknife-compiler:8.4.0' - testCompile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.10.0' testCompile 'org.assertj:assertj-core:3.8.0' + testCompile "com.nhaarman:mockito-kotlin-kt1.1:1.5.0" androidTestCompile 'com.android.support.test:runner:1.0.1' androidTestCompile 'com.android.support.test:rules:1.0.1' diff --git a/app/src/main/kotlin/com/example/unittesting/BasePresenter.java b/app/src/main/kotlin/com/example/unittesting/BasePresenter.java deleted file mode 100644 index 32b6450..0000000 --- a/app/src/main/kotlin/com/example/unittesting/BasePresenter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.unittesting; - -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; - -public class BasePresenter implements Presenter { - - T view; - CompositeDisposable compositeDisposable = new CompositeDisposable(); - - @Override - public void createView(T view) { - this.view = view; - } - - @Override - public void destroyView() { - compositeDisposable.clear(); - view = null; - } - - protected void bindToLifecycle(Disposable disposable) { - compositeDisposable.add(disposable); - } - - protected T getView() { - return view; - } -} diff --git a/app/src/main/kotlin/com/example/unittesting/BasePresenter.kt b/app/src/main/kotlin/com/example/unittesting/BasePresenter.kt new file mode 100644 index 0000000..83d66ec --- /dev/null +++ b/app/src/main/kotlin/com/example/unittesting/BasePresenter.kt @@ -0,0 +1,23 @@ +package com.example.unittesting + +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable + +open class BasePresenter : Presenter { + + var view: T? = null + private val compositeDisposable = CompositeDisposable() + + override fun createView(view: T) { + this.view = view + } + + fun bindToLifecycle(disposable: Disposable) { + compositeDisposable.add(disposable) + } + + override fun destroyView() { + compositeDisposable.clear() + view = null + } +} diff --git a/app/src/main/kotlin/com/example/unittesting/Presenter.java b/app/src/main/kotlin/com/example/unittesting/Presenter.java deleted file mode 100644 index 8c738c1..0000000 --- a/app/src/main/kotlin/com/example/unittesting/Presenter.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.unittesting; - -interface Presenter { - - void createView(T view); - - void destroyView(); -} diff --git a/app/src/main/kotlin/com/example/unittesting/Presenter.kt b/app/src/main/kotlin/com/example/unittesting/Presenter.kt new file mode 100644 index 0000000..b4e75f2 --- /dev/null +++ b/app/src/main/kotlin/com/example/unittesting/Presenter.kt @@ -0,0 +1,8 @@ +package com.example.unittesting + +internal interface Presenter { + + fun createView(view: T) + + fun destroyView() +} diff --git a/app/src/main/kotlin/com/example/unittesting/ResourceProvider.java b/app/src/main/kotlin/com/example/unittesting/ResourceProvider.java deleted file mode 100644 index 2487f60..0000000 --- a/app/src/main/kotlin/com/example/unittesting/ResourceProvider.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.unittesting; - -import android.content.res.Resources; -import android.support.annotation.StringRes; - -public class ResourceProvider { - - Resources resources; - - public ResourceProvider(Resources resources) { - this.resources = resources; - } - - public String getString(@StringRes int stringResId) { - return resources.getString(stringResId); - } -} diff --git a/app/src/main/kotlin/com/example/unittesting/ResourceProvider.kt b/app/src/main/kotlin/com/example/unittesting/ResourceProvider.kt new file mode 100644 index 0000000..0c70eea --- /dev/null +++ b/app/src/main/kotlin/com/example/unittesting/ResourceProvider.kt @@ -0,0 +1,11 @@ +package com.example.unittesting + +import android.content.res.Resources +import android.support.annotation.StringRes + +class ResourceProvider(private var resources: Resources) { + + fun getString(@StringRes stringResId: Int): String { + return resources.getString(stringResId) + } +} diff --git a/app/src/main/kotlin/com/example/unittesting/SchedulersFactory.java b/app/src/main/kotlin/com/example/unittesting/SchedulersFactory.java deleted file mode 100644 index 41ba4f4..0000000 --- a/app/src/main/kotlin/com/example/unittesting/SchedulersFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.unittesting; - -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.ObservableTransformer; -import io.reactivex.android.schedulers.AndroidSchedulers; - -public class SchedulersFactory { - - public ObservableTransformer createMainThreadSchedulerTransformer() { - return new SchedulersTransformer<>(); - } - - static class SchedulersTransformer implements ObservableTransformer { - - @Override - public ObservableSource apply(Observable upstream) { - return upstream.observeOn(AndroidSchedulers.mainThread()); - } - } -} diff --git a/app/src/main/kotlin/com/example/unittesting/SchedulersFactory.kt b/app/src/main/kotlin/com/example/unittesting/SchedulersFactory.kt new file mode 100644 index 0000000..9ae4078 --- /dev/null +++ b/app/src/main/kotlin/com/example/unittesting/SchedulersFactory.kt @@ -0,0 +1,20 @@ +package com.example.unittesting + +import io.reactivex.Observable +import io.reactivex.ObservableSource +import io.reactivex.ObservableTransformer +import io.reactivex.android.schedulers.AndroidSchedulers + +class SchedulersFactory { + + fun createMainThreadSchedulerTransformer(): ObservableTransformer { + return SchedulersTransformer() + } +} + +internal class SchedulersTransformer : ObservableTransformer { + + override fun apply(upstream: Observable): ObservableSource { + return upstream.observeOn(AndroidSchedulers.mainThread()) + } +} diff --git a/app/src/main/kotlin/com/example/unittesting/login/model/LoginCredentials.java b/app/src/main/kotlin/com/example/unittesting/login/model/LoginCredentials.java deleted file mode 100644 index edd1de4..0000000 --- a/app/src/main/kotlin/com/example/unittesting/login/model/LoginCredentials.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.unittesting.login.model; - -public class LoginCredentials { - - public String login; - public String password; - - public LoginCredentials withLogin(String login) { - this.login = login; - return this; - } - - public LoginCredentials withPassword(String password) { - this.password = password; - return this; - } -} diff --git a/app/src/main/kotlin/com/example/unittesting/login/model/LoginCredentials.kt b/app/src/main/kotlin/com/example/unittesting/login/model/LoginCredentials.kt new file mode 100644 index 0000000..c222e08 --- /dev/null +++ b/app/src/main/kotlin/com/example/unittesting/login/model/LoginCredentials.kt @@ -0,0 +1,3 @@ +package com.example.unittesting.login.model + +data class LoginCredentials(val login: String, val password: String) diff --git a/app/src/main/kotlin/com/example/unittesting/login/model/LoginRepository.java b/app/src/main/kotlin/com/example/unittesting/login/model/LoginRepository.java deleted file mode 100644 index f47e722..0000000 --- a/app/src/main/kotlin/com/example/unittesting/login/model/LoginRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.unittesting.login.model; - -import java.util.concurrent.TimeUnit; - -import io.reactivex.Observable; -import timber.log.Timber; - -public class LoginRepository { - - static final String CORRECT_LOGIN = "dbacinski"; - static final String CORRECT_PASSWORD = "correct"; - - public Observable login(String login, String password) { - Timber.v("login %s with password %s", login, password); - - return Observable.just( - CORRECT_LOGIN.equals(login) && CORRECT_PASSWORD.equals(password) - ).delay(500, TimeUnit.MILLISECONDS); - } -} diff --git a/app/src/main/kotlin/com/example/unittesting/login/model/LoginRepository.kt b/app/src/main/kotlin/com/example/unittesting/login/model/LoginRepository.kt new file mode 100644 index 0000000..f44c8df --- /dev/null +++ b/app/src/main/kotlin/com/example/unittesting/login/model/LoginRepository.kt @@ -0,0 +1,21 @@ +package com.example.unittesting.login.model + +import io.reactivex.Observable +import io.reactivex.Observable.fromCallable +import timber.log.Timber +import java.util.concurrent.TimeUnit + +class LoginRepository { + + fun login(login: String, password: String): Observable { + Timber.v("login %s with password %s", login, password) + + return fromCallable { CORRECT_LOGIN == login && CORRECT_PASSWORD == password } + .delay(2000, TimeUnit.MILLISECONDS) + } + + companion object { + internal val CORRECT_LOGIN = "dbacinski" + internal val CORRECT_PASSWORD = "correct" + } +} diff --git a/app/src/main/kotlin/com/example/unittesting/login/model/LoginUseCase.java b/app/src/main/kotlin/com/example/unittesting/login/model/LoginUseCase.java deleted file mode 100644 index 7be48e5..0000000 --- a/app/src/main/kotlin/com/example/unittesting/login/model/LoginUseCase.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.unittesting.login.model; - -import io.reactivex.Observable; - -public class LoginUseCase { - - LoginRepository loginRepository; - - public LoginUseCase(LoginRepository loginRepository) { - this.loginRepository = loginRepository; - } - - public Observable loginWithCredentialsWithStatus(final LoginCredentials credentials) { - checkNotNull(credentials); - return loginRepository.login(credentials.login, credentials.password); - } - - private void checkNotNull(LoginCredentials credentials) { - if (credentials == null) { - throw new NullPointerException("Credentials cannot be null"); - } - } -} diff --git a/app/src/main/kotlin/com/example/unittesting/login/model/LoginUseCase.kt b/app/src/main/kotlin/com/example/unittesting/login/model/LoginUseCase.kt new file mode 100644 index 0000000..c65b11e --- /dev/null +++ b/app/src/main/kotlin/com/example/unittesting/login/model/LoginUseCase.kt @@ -0,0 +1,10 @@ +package com.example.unittesting.login.model + +import io.reactivex.Observable + +class LoginUseCase(private val loginRepository: LoginRepository) { + + fun loginWithCredentialsWithStatus(credentials: LoginCredentials): Observable { + return loginRepository.login(credentials.login, credentials.password) + } +} diff --git a/app/src/main/kotlin/com/example/unittesting/login/model/LoginValidator.java b/app/src/main/kotlin/com/example/unittesting/login/model/LoginValidator.java deleted file mode 100644 index aa4490f..0000000 --- a/app/src/main/kotlin/com/example/unittesting/login/model/LoginValidator.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.unittesting.login.model; - -public class LoginValidator { - - static final String EMPTY = ""; - static final int MIN_PASSWORD_LENGTH = 6; - - public boolean validateLogin(String login) { - return !login.equals(EMPTY); - } - - public boolean validatePassword(String password) { - return password.length() >= MIN_PASSWORD_LENGTH; - } -} diff --git a/app/src/main/kotlin/com/example/unittesting/login/model/LoginValidator.kt b/app/src/main/kotlin/com/example/unittesting/login/model/LoginValidator.kt new file mode 100644 index 0000000..e00804c --- /dev/null +++ b/app/src/main/kotlin/com/example/unittesting/login/model/LoginValidator.kt @@ -0,0 +1,17 @@ +package com.example.unittesting.login.model + +class LoginValidator { + + companion object { + val EMPTY = "" + val MIN_PASSWORD_LENGTH = 6 + } + + fun validateLogin(login: String): Boolean { + return login != EMPTY + } + + fun validatePassword(password: String): Boolean { + return password.length >= MIN_PASSWORD_LENGTH + } +} diff --git a/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginPresenter.java b/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginPresenter.java deleted file mode 100644 index 7b94f85..0000000 --- a/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginPresenter.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.example.unittesting.login.presenter; - -import com.example.unittesting.R; -import com.example.unittesting.ResourceProvider; -import com.example.unittesting.SchedulersFactory; -import com.example.unittesting.BasePresenter; -import com.example.unittesting.login.model.LoginCredentials; -import com.example.unittesting.login.model.LoginUseCase; -import com.example.unittesting.login.model.LoginValidator; - -import io.reactivex.functions.Consumer; - -public class LoginPresenter extends BasePresenter { - - ResourceProvider resourceProvider; - LoginValidator loginValidator; - LoginUseCase loginUseCase; - SchedulersFactory schedulersFactory; - - public LoginPresenter(ResourceProvider resourceProvider, LoginValidator loginValidator, LoginUseCase loginUseCase, SchedulersFactory schedulersFactory) { - this.resourceProvider = resourceProvider; - this.loginValidator = loginValidator; - this.loginUseCase = loginUseCase; - this.schedulersFactory = schedulersFactory; - } - - public void attemptLogin(LoginCredentials loginCredentials) { - - boolean validationError = validatePassword(loginCredentials, false); - - validationError = validateLogin(loginCredentials, validationError); - - if (validationError) { - return; - } - - getView().showProgress(); - - loginUseCase.loginWithCredentialsWithStatus(loginCredentials) - .compose(schedulersFactory.createMainThreadSchedulerTransformer()) - .subscribe(new Consumer() { - - @Override - public void accept(Boolean success) throws Exception { - getView().hideProgress(); - - if (success) { - getView().onLoginSuccessful(); - } else { - getView().showPasswordError(resourceProvider.getString(R.string.error_incorrect_password)); - getView().requestPasswordFocus(); - } - } - }); - } - - private boolean validatePassword(LoginCredentials loginCredentials, boolean validationError) { - if (!loginValidator.validatePassword(loginCredentials.password)) { - getView().showPasswordError(resourceProvider.getString(R.string.error_invalid_password)); - getView().requestPasswordFocus(); - validationError = true; - } else { - getView().showPasswordError(null); - } - return validationError; - } - - private boolean validateLogin(LoginCredentials loginCredentials, boolean validationError) { - if (!loginValidator.validateLogin(loginCredentials.login)) { - getView().showLoginError(resourceProvider.getString(R.string.error_field_required)); - getView().requestLoginFocus(); - validationError = true; - } else { - getView().showLoginError(null); - } - return validationError; - } -} diff --git a/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginPresenter.kt b/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginPresenter.kt new file mode 100644 index 0000000..b407c05 --- /dev/null +++ b/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginPresenter.kt @@ -0,0 +1,57 @@ +package com.example.unittesting.login.presenter + +import com.example.unittesting.BasePresenter +import com.example.unittesting.R +import com.example.unittesting.ResourceProvider +import com.example.unittesting.SchedulersFactory +import com.example.unittesting.login.model.LoginCredentials +import com.example.unittesting.login.model.LoginUseCase +import com.example.unittesting.login.model.LoginValidator + +class LoginPresenter(private val resourceProvider: ResourceProvider, private val loginValidator: LoginValidator, private val loginUseCase: LoginUseCase, private val schedulersFactory: SchedulersFactory) : BasePresenter() { + + fun attemptLogin(loginCredentials: LoginCredentials) { + if (!validateInputs(loginCredentials)) { + return + } + + view?.showProgress() + loginUseCase.loginWithCredentialsWithStatus(loginCredentials) + .compose(schedulersFactory.createMainThreadSchedulerTransformer()) + .subscribe { success -> + view?.hideProgress() + + if (success) { + view?.onLoginSuccessful() + } else { + view?.showPasswordError(resourceProvider.getString(R.string.error_incorrect_password)) + view?.requestPasswordFocus() + } + } + } + + private fun validateInputs(loginCredentials: LoginCredentials): Boolean { + val validateLogin = validateLogin(loginCredentials) + return validatePassword(loginCredentials) && validateLogin //XXX validateLogin is not inlined to avoid short circuit check + } + + private fun validateLogin(loginCredentials: LoginCredentials): Boolean = + if (loginValidator.validateLogin(loginCredentials.login)) { + view?.showLoginError(null) + true + } else { + view?.showLoginError(resourceProvider.getString(R.string.error_field_required)) + view?.requestLoginFocus() + false + } + + private fun validatePassword(loginCredentials: LoginCredentials): Boolean = + if (loginValidator.validatePassword(loginCredentials.password)) { + view?.showPasswordError(null) + true + } else { + view?.showPasswordError(resourceProvider.getString(R.string.error_invalid_password)) + view?.requestPasswordFocus() + false + } +} diff --git a/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginView.java b/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginView.java deleted file mode 100644 index 0ff6ef2..0000000 --- a/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginView.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.unittesting.login.presenter; - -public interface LoginView { - - void showProgress(); - - void hideProgress(); - - void onLoginSuccessful(); - - void showLoginError(String errorMessage); - - void showPasswordError(String errorMessage); - - void requestLoginFocus(); - - void requestPasswordFocus(); -} diff --git a/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginView.kt b/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginView.kt new file mode 100644 index 0000000..31399c1 --- /dev/null +++ b/app/src/main/kotlin/com/example/unittesting/login/presenter/LoginView.kt @@ -0,0 +1,18 @@ +package com.example.unittesting.login.presenter + +interface LoginView { + + fun showProgress() + + fun hideProgress() + + fun onLoginSuccessful() + + fun showLoginError(errorMessage: String?) + + fun showPasswordError(errorMessage: String?) + + fun requestLoginFocus() + + fun requestPasswordFocus() +} diff --git a/app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.java b/app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.java deleted file mode 100644 index ac15acb..0000000 --- a/app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.example.unittesting.login.view; - -import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; -import android.view.View; -import android.widget.EditText; - -import com.example.unittesting.R; -import com.example.unittesting.ResourceProvider; -import com.example.unittesting.SchedulersFactory; -import com.example.unittesting.login.model.LoginCredentials; -import com.example.unittesting.login.model.LoginRepository; -import com.example.unittesting.login.model.LoginUseCase; -import com.example.unittesting.login.model.LoginValidator; -import com.example.unittesting.login.presenter.LoginPresenter; -import com.example.unittesting.login.presenter.LoginView; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; - -/** - * Based on Google Login Screen example - */ -public class LoginActivity extends AppCompatActivity implements LoginView { - - @BindView(R.id.email) - EditText loginView; - @BindView(R.id.password) - EditText passwordView; - @BindView(R.id.login_progress) - View progressView; - @BindView(R.id.login_form) - View loginFormView; - - //TODO @Inject - LoginPresenter loginPresenter; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_login); - - ButterKnife.bind(this); - - passwordView.setOnEditorActionListener((textView, id, keyEvent) -> { - if (id == R.id.login) { - onSignInClick(); - return true; - } - return false; - }); - - loginPresenter = new LoginPresenter(new ResourceProvider(getResources()), new LoginValidator(), new LoginUseCase(new LoginRepository()), new SchedulersFactory()); - - loginPresenter.createView(this); - } - - @Override - protected void onDestroy() { - loginPresenter.destroyView(); - super.onDestroy(); - } - - @OnClick(R.id.email_sign_in_button) - public void onSignInClick() { - loginPresenter.attemptLogin(new LoginCredentials() - .withLogin(loginView.getText().toString()) - .withPassword(passwordView.getText().toString())); - } - - @Override - public void showProgress() { - showProgress(true); - } - - @Override - public void hideProgress() { - showProgress(false); - } - - @Override - public void onLoginSuccessful() { - finish(); - } - - @Override - public void showLoginError(String errorMessage) { - loginView.setError(errorMessage); - } - - @Override - public void showPasswordError(String errorMessage) { - passwordView.setError(errorMessage); - } - - @Override - public void requestLoginFocus() { - loginView.requestFocus(); - } - - @Override - public void requestPasswordFocus() { - passwordView.requestFocus(); - } - - void showProgress(boolean progressVisible) { - int animationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime); - - loginFormView.setVisibility(progressVisible ? View.GONE : View.VISIBLE); - loginFormView.animate().setDuration(animationDuration).alpha(progressVisible ? 0 : 1); - - progressView.setVisibility(progressVisible ? View.VISIBLE : View.GONE); - progressView.animate().setDuration(animationDuration).alpha(progressVisible ? 1 : 0); - } -} - diff --git a/app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.kt b/app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.kt new file mode 100644 index 0000000..222fd6b --- /dev/null +++ b/app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.kt @@ -0,0 +1,109 @@ +package com.example.unittesting.login.view + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.view.View +import android.widget.EditText +import butterknife.BindView +import butterknife.ButterKnife +import butterknife.OnClick +import com.example.unittesting.R +import com.example.unittesting.ResourceProvider +import com.example.unittesting.SchedulersFactory +import com.example.unittesting.login.model.LoginCredentials +import com.example.unittesting.login.model.LoginRepository +import com.example.unittesting.login.model.LoginUseCase +import com.example.unittesting.login.model.LoginValidator +import com.example.unittesting.login.presenter.LoginPresenter +import com.example.unittesting.login.presenter.LoginView + +/** + * Based on Google Login Screen example + */ +class LoginActivity : AppCompatActivity(), LoginView { + + @BindView(R.id.email) + lateinit var loginView: EditText + @BindView(R.id.password) + lateinit var passwordView: EditText + @BindView(R.id.login_progress) + lateinit var progressView: View + @BindView(R.id.login_form) + lateinit var loginFormView: View + + //TODO @Inject + lateinit var loginPresenter: LoginPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + + ButterKnife.bind(this) + + passwordView.setOnEditorActionListener { _, id, _ -> + if (id == R.id.login) { + onSignInClick() + true + } else { + false + } + } + + loginPresenter = LoginPresenter(ResourceProvider(resources), LoginValidator(), LoginUseCase(LoginRepository()), SchedulersFactory()) + loginPresenter.createView(this) + } + + override fun onDestroy() { + loginPresenter.destroyView() + super.onDestroy() + } + + @OnClick(R.id.email_sign_in_button) + fun onSignInClick() { + loginPresenter.attemptLogin( + LoginCredentials( + login = loginView.text.toString(), + password = passwordView.text.toString() + ) + ) + } + + override fun showProgress() { + showProgress(true) + } + + override fun hideProgress() { + showProgress(false) + } + + override fun onLoginSuccessful() { + finish() + } + + override fun showLoginError(errorMessage: String?) { + loginView.error = errorMessage + } + + override fun showPasswordError(errorMessage: String?) { + passwordView.error = errorMessage + } + + override fun requestLoginFocus() { + loginView.requestFocus() + } + + override fun requestPasswordFocus() { + passwordView.requestFocus() + } + + internal fun showProgress(progressVisible: Boolean) { + val animationDuration = resources.getInteger(android.R.integer.config_shortAnimTime) + + loginFormView.visibility = if (progressVisible) View.GONE else View.VISIBLE + loginFormView.animate().setDuration(animationDuration.toLong()).alpha((if (progressVisible) 0 else 1).toFloat()) + + progressView.visibility = if (progressVisible) View.VISIBLE else View.GONE + progressView.animate().setDuration(animationDuration.toLong()).alpha((if (progressVisible) 1 else 0).toFloat()) + } +} + diff --git a/app/src/test/kotlin/com/example/unittesting/BasePresenterTest.kt b/app/src/test/kotlin/com/example/unittesting/BasePresenterTest.kt index 81b8770..9336cab 100644 --- a/app/src/test/kotlin/com/example/unittesting/BasePresenterTest.kt +++ b/app/src/test/kotlin/com/example/unittesting/BasePresenterTest.kt @@ -27,7 +27,7 @@ class BasePresenterTest { //when objectUnderTest.destroyView() //then - assertThat(objectUnderTest.getView()).isNull() + assertThat(objectUnderTest.view).isNull() } } diff --git a/app/src/test/kotlin/com/example/unittesting/login/model/LoginRepositoryTest.kt b/app/src/test/kotlin/com/example/unittesting/login/model/LoginRepositoryTest.kt index c6a762b..c0bd3c9 100644 --- a/app/src/test/kotlin/com/example/unittesting/login/model/LoginRepositoryTest.kt +++ b/app/src/test/kotlin/com/example/unittesting/login/model/LoginRepositoryTest.kt @@ -1,7 +1,7 @@ package com.example.unittesting.login.model -import com.example.unittesting.login.model.LoginRepository.CORRECT_LOGIN -import com.example.unittesting.login.model.LoginRepository.CORRECT_PASSWORD +import com.example.unittesting.login.model.LoginRepository.Companion.CORRECT_LOGIN +import com.example.unittesting.login.model.LoginRepository.Companion.CORRECT_PASSWORD import org.junit.Test class LoginRepositoryTest { diff --git a/app/src/test/kotlin/com/example/unittesting/login/model/LoginUseCaseTest.kt b/app/src/test/kotlin/com/example/unittesting/login/model/LoginUseCaseTest.kt deleted file mode 100644 index f3d559e..0000000 --- a/app/src/test/kotlin/com/example/unittesting/login/model/LoginUseCaseTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.unittesting.login.model - -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.catchThrowable -import org.junit.Test -import org.mockito.Mockito - -class LoginUseCaseTest { - - val objectUnderTest = LoginUseCase(Mockito.mock(LoginRepository::class.java)) - - @Test - fun `throws exception for null LoginCredentials`() { - //given - val loginCredentials = null - //when - val catchThrowable = catchThrowable { objectUnderTest.loginWithCredentialsWithStatus(loginCredentials) } - //then - assertThat(catchThrowable).isInstanceOf(NullPointerException::class.java) - } -} \ No newline at end of file diff --git a/app/src/test/kotlin/com/example/unittesting/login/model/LoginValidatorTest.kt b/app/src/test/kotlin/com/example/unittesting/login/model/LoginValidatorTest.kt index 74d4335..d2c0b64 100644 --- a/app/src/test/kotlin/com/example/unittesting/login/model/LoginValidatorTest.kt +++ b/app/src/test/kotlin/com/example/unittesting/login/model/LoginValidatorTest.kt @@ -1,7 +1,6 @@ package com.example.unittesting.login.model import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.catchThrowable import org.junit.Test class LoginValidatorTest { @@ -24,14 +23,6 @@ class LoginValidatorTest { assertThat(result).isTrue() } - @Test - fun `throws exception for null login`() { - //when - val catchThrowable = catchThrowable { objectUnderTest.validateLogin(null) } - //then - assertThat(catchThrowable).isInstanceOf(NullPointerException::class.java) - } - @Test fun `empty password is invalid`() { //when @@ -40,14 +31,6 @@ class LoginValidatorTest { assertThat(result).isFalse() } - @Test - fun `throws exception for null password`() { - //when - val catchThrowable = catchThrowable { objectUnderTest.validatePassword(null) } - //then - assertThat(catchThrowable).isInstanceOf(NullPointerException::class.java) - } - @Test fun `password is invalid if shorter then limit`() { //when diff --git a/app/src/test/kotlin/com/example/unittesting/login/presenter/LoginPresenterTest.kt b/app/src/test/kotlin/com/example/unittesting/login/presenter/LoginPresenterTest.kt index 8567b78..52e9c00 100644 --- a/app/src/test/kotlin/com/example/unittesting/login/presenter/LoginPresenterTest.kt +++ b/app/src/test/kotlin/com/example/unittesting/login/presenter/LoginPresenterTest.kt @@ -7,6 +7,7 @@ import com.example.unittesting.login.model.LoginCredentials import com.example.unittesting.login.model.LoginRepository import com.example.unittesting.login.model.LoginUseCase import com.example.unittesting.login.model.LoginValidator +import com.nhaarman.mockito_kotlin.any import io.reactivex.Observable import io.reactivex.ObservableTransformer import org.junit.Before @@ -35,7 +36,7 @@ class LoginPresenterTest { //given given(loginRepositoryStub.login(any(), any())).willReturn(Observable.just(true)) //when - objectUnderTest.attemptLogin(LoginCredentials().withLogin("correct").withPassword("correct")) + objectUnderTest.attemptLogin(LoginCredentials(login = "correct", password = "correct")) //then verify(loginViewMock).onLoginSuccessful() } @@ -45,7 +46,7 @@ class LoginPresenterTest { //given given(loginRepositoryStub.login(any(), any())).willReturn(Observable.just(true)) //when - objectUnderTest.attemptLogin(LoginCredentials().withLogin("correct").withPassword("correct")) + objectUnderTest.attemptLogin(LoginCredentials(login = "correct", password = "correct")) //then val ordered = inOrder(loginViewMock) ordered.verify(loginViewMock).showProgress() @@ -58,7 +59,7 @@ class LoginPresenterTest { given(resourcesStub.getString(anyInt())).willReturn("error") given(loginRepositoryStub.login(any(), any())).willReturn(Observable.just(false)) //when - objectUnderTest.attemptLogin(LoginCredentials().withLogin("valid").withPassword("incorrectPassword")) + objectUnderTest.attemptLogin(LoginCredentials(login = "valid", password = "incorrectPassword")) //then val ordered = inOrder(loginViewMock) ordered.verify(loginViewMock).showLoginError(null) @@ -71,7 +72,7 @@ class LoginPresenterTest { given(resourcesStub.getString(anyInt())).willReturn("error") val login = "" //when - objectUnderTest.attemptLogin(LoginCredentials().withLogin(login).withPassword("validPassword")) + objectUnderTest.attemptLogin(LoginCredentials(login = login, password = "validPassword")) //then verify(loginViewMock).showLoginError("error") verify(loginViewMock).showPasswordError(null) @@ -84,7 +85,7 @@ class LoginPresenterTest { val login = "" val password = "short" //when - objectUnderTest.attemptLogin(LoginCredentials().withLogin(login).withPassword(password)) + objectUnderTest.attemptLogin(LoginCredentials(login = login, password = password)) //then verify(loginViewMock).showLoginError("error") verify(loginViewMock).showPasswordError("error") @@ -96,7 +97,7 @@ class LoginPresenterTest { given(resourcesStub.getString(anyInt())).willReturn("error") val password = "short" //when - objectUnderTest.attemptLogin(LoginCredentials().withLogin("valid").withPassword(password)) + objectUnderTest.attemptLogin(LoginCredentials(login = "valid", password = password)) //then verify(loginViewMock).showLoginError(null) verify(loginViewMock).showPasswordError("error") diff --git a/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..ca6ee9c --- /dev/null +++ b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file From 6ad3d343186851006266c5a930435bc61526a892 Mon Sep 17 00:00:00 2001 From: Dariusz Bacinski Date: Fri, 3 Nov 2017 21:14:08 +0100 Subject: [PATCH 2/4] updated circle ci config with latest build tools --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index cb0a6c1..3772f14 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ dependencies: pre: - - echo y | android update sdk --no-ui --all --filter "android-24,build-tools-25.0.2" + - echo y | android update sdk --no-ui --all --filter "android-27,build-tools-26.0.2" - chmod +x gradlew test: override: From f1d19b494e8e290d502cba7e28a5b0ce111c1579 Mon Sep 17 00:00:00 2001 From: Dariusz Bacinski Date: Fri, 3 Nov 2017 21:32:00 +0100 Subject: [PATCH 3/4] fixed lint error - ime action id --- .../kotlin/com/example/unittesting/login/view/LoginActivity.kt | 2 +- app/src/main/res/layout/activity_login.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.kt b/app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.kt index 222fd6b..e6b5d63 100644 --- a/app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.kt +++ b/app/src/main/kotlin/com/example/unittesting/login/view/LoginActivity.kt @@ -41,7 +41,7 @@ class LoginActivity : AppCompatActivity(), LoginView { ButterKnife.bind(this) passwordView.setOnEditorActionListener { _, id, _ -> - if (id == R.id.login) { + if (id == 1) { onSignInClick() true } else { diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 04c19e2..88bc029 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -52,7 +52,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/prompt_password" - android:imeActionId="@+id/login" + android:imeActionId="1" android:imeActionLabel="@string/action_sign_in_short" android:imeOptions="actionUnspecified" android:inputType="textPassword" From 21b838caedb051ea0e9348e3009393d5ed04e222 Mon Sep 17 00:00:00 2001 From: Dariusz Bacinski Date: Fri, 3 Nov 2017 21:49:06 +0100 Subject: [PATCH 4/4] fixed artifact path --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 3772f14..04865b1 100644 --- a/circle.yml +++ b/circle.yml @@ -5,7 +5,7 @@ dependencies: test: override: - ./gradlew build test - - cp ./*/build/outputs/apk/*.apk $CIRCLE_ARTIFACTS/ + - cp ./*/build/outputs/apk/release/*.apk $CIRCLE_ARTIFACTS/ - cp ./*/build/test-results/testDebugUnitTest/*.xml $CIRCLE_TEST_REPORTS/ post: - bash <(curl -s https://codecov.io/bash)