Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Droidcon Vienna 2018 - Architecture and Framewo...

Droidcon Vienna 2018 - Architecture and Framework for Clean Test Code

The talk, presented by Marc Neureiter from mySugr, focuses on improving test design and software architecture for mobile app development, specifically using mySugr's journey as an example. It highlights the benefits of automation over manual testing, including scalability, speed, and maintainability, using modular and clean architecture to support these goals. The framework "sweetest" is introduced as a tool to simplify unit and integration tests, promoting maintainability, readability, and reusability. Behavior-driven development (BDD) is also discussed as a methodology to enhance testing and improve the development process. Overall, the talk emphasizes how effective testing and architecture contribute to better quality, faster iteration, and long-term project success.

Avatar for Marc Neureiter

Marc Neureiter

September 21, 2018
Tweet

More Decks by Marc Neureiter

Other Decks in Programming

Transcript

  1. facts Vienna & San Diego +100 employees 25 people with

    diabetes part of Roche since June 2017
  2. MESS LoginVM AuthManager BackendGW LoginVMTest Setup Mock Interact AuthManagerTest Setup

    Mock Interact Assert LoginIntegrationTest Setup Mock Interact Assert Assert
  3. private val httpService = mock<LoginHttpService>() private val authStringGenerator = mock<AuthStringGenerator>()

    private val resourceProvider = mock<ResourceProvider>() private val navigator = mock<LoginNavigator>() private val eventBus = mock<EventBus>() private lateinit var user: User private lateinit var syncCoordinator: SyncCoordinator private lateinit var sut: LoginViewModel private lateinit var loginService: LoginService private val scheduler = TestSchedulerProvider() MESS
  4. @Before fun before() { syncCoordinator = SyncCoordinatorImpl(mutableListOf(), mock<ConnectivityUtil>(), TestSchedulerProvi user

    = User().apply { emailAddress = EMAIL } loginService = LoginService(mock<ClientInfo>(), syncCoordinator, mock<UserSettings>(), mock<UserPreferences>(), eventBus, httpService, mock<BackendUrlStore>(), authStringGenerator) Tracker.init(mock<TrackerEngine>()) sut = LoginViewModel(scheduler, mock<Logger>(), loginService, resourceProvider).apply { bind(navigator, null, null) userInput.emailAddress = EMAIL userInput.password = PASSWORD } // [...] MESS
  5. @Before fun before() { // [...] `when`(authStringGenerator.getAuthStringFromCredential(any() ?: "", any()

    ?: "")) .thenReturn(AUTH_STRING) `when`(httpService.getUser(any() ?: "", any() ?: "", any() ?: "", any() ?: "", any() ?: "")) .thenReturn(just(UserWrapper().apply { user = [email protected] })) `when`(httpService.postUserInput(any() ?: "", any() ?: UserInputWrapper(UserInput()), any() ?: "", .thenReturn(just(Response.success(UserWrapper().apply { user = this@LoginViewModelIntegrati `when`(resourceProvider.getString(any() ?: 0)) .thenReturn(RESOURCE_STRING) `when`(resourceProvider.locale) .thenReturn(Locale.GERMAN) } MESS
  6. @Test fun whenEmailEntered_thenServiceGatewayIsCalled() { `when`(httpService.probeUser(any() ?: "", any() ?: "",

    any() ?: "", any() ?: "", any() ?: "", any() .thenReturn(just<Response<Any>?>(Response.success(Any()))) sut.onEmailEntered() verify(httpService).probeUser(any() ?: "", any() ?: "", any() ?: "", any() ?: "", any() ?: "", any( } @Test fun whenEmailEnteredAndNotFound_thenVerificationCodeIsRequested() { `when`(httpService.probeUser(any() ?: "", any() ?: "", any() ?: "", any() ?: "", any() ?: "", any() .thenReturn(just<Response<Any>?>(Response.error(404, LoginServiceTests.emailVerificationEna `when`(httpService.generateVerificationCode(any() ?: "", any() ?: EmailVerificationWrapper(), any() .thenReturn(just(Response.success(null))) sut.onEmailEntered() verify(httpService).generateVerificationCode(any() ?: "", any() ?: EmailVerificationWrapper(), any( } @Test fun whenEmailEnteredAndNotFoundAndVerificationRequestFailed_thenAnErrorMessageIsShown() { `when`(httpService.probeUser(any() ?: "", any() ?: "", any() ?: "", any() ?: "", any() ?: "", any() .thenReturn(just<Response<Any>?>(Response.error(404, LoginServiceTests.emailVerificationEna `when`(httpService.generateVerificationCode(any() ?: "", any() ?: EmailVerificationWrapper(), any() MESS
  7. AuthManagerSteps Mock Assert LoginVM AuthManager BackendGW LoginVMSteps Setup Mock Interact

    Assert LoginVMTest Setup Mock Interact Assert LoginIntegrationTest Setup Mock Interact Assert BackendGWSteps Mock Assert Setup Interact AuthManagerTest Setup Mock Interact Assert
  8. @Test fun `Enter email address as new user`() { user.exists

    = false sut.enterEmail() http { verifyProbeUserCalled() verifyGenerateVerificationCodeCalled() } sut.verifyLoginModelStateIs(State.EMAIL_VERIFICATION) } @Test fun `Enter email address as existing user`() { user.exists = true sut { enterEmail() verifyLoginModelStateIs(State.LOGIN) } } @Test fun `Enter email address as existing user fails`() { user.exists = true connectivity.requestsSucceed = false unit test MUCH BETTER
  9. fun whenLoggingInOrRegistering() { instance.loginOrRegister(user.email, user.password) } fun whenLoggingOut() { instance.logout()

    } fun thenLoginOrRegisterIsCalled() { verify(instance).loginOrRegister(user.email, user.password) } fun thenLoginOrRegisterThrows() { assertNotNull(loginOrRegisterThrowable) } step definitions MUCH BETTER
  10. @Test fun whenEmailEntered_thenServiceGatewayIsCalled() { `when`(httpService.probeUser(any() ?: "", any() ?: "",

    any() ?: "", any() ?: "", any() ?: "", any() .thenReturn(just<Response<Any>?>(Response.success(Any()))) sut.onEmailEntered() verify(httpService).probeUser(any() ?: "", any() ?: "", any() ?: "", any() ?: "", any() ?: "", any( } @Test fun whenEmailEnteredAndNotFound_thenVerificationCodeIsRequested() { `when`(httpService.probeUser(any() ?: "", any() ?: "", any() ?: "", any() ?: "", any() ?: "", any() .thenReturn(just<Response<Any>?>(Response.error(404, LoginServiceTests.emailVerificationEna `when`(httpService.generateVerificationCode(any() ?: "", any() ?: EmailVerificationWrapper(), any() .thenReturn(just(Response.success(null))) sut.onEmailEntered() verify(httpService).generateVerificationCode(any() ?: "", any() ?: EmailVerificationWrapper(), any( } @Test fun whenEmailEnteredAndNotFoundAndVerificationRequestFailed_thenAnErrorMessageIsShown() { `when`(httpService.probeUser(any() ?: "", any() ?: "", any() ?: "", any() ?: "", any() ?: "", any() .thenReturn(just<Response<Any>?>(Response.error(404, LoginServiceTests.emailVerificationEna `when`(httpService.generateVerificationCode(any() ?: "", any() ?: EmailVerificationWrapper(), any() MUCH BETTER
  11. AuthManagerSteps Mock Assert LoginVM AuthManager BackendGW LoginVMSteps Setup Mock Interact

    Assert BackendGWSteps Mock Assert Setup Interact LoginVMTest Setup Mock Interact Assert
  12. class LoginViewModelTest : BaseJUnitTest(appModuleTestingConfiguration) { override fun configure() = super.configure()

    .requireReal<LoginViewModel>() private val sut by steps<LoginViewModelSteps>() private val navigator by steps<LoginNavigatorSteps>() private val loginService by steps<LoginServiceSteps>() private val user by steps<UserSteps>() private val features by steps<FeaturesSteps>() private val connectivity by steps<ConnectivitySteps>() @Test fun `Enter email address as new user`() { user.exists = false sut.enterEmail() http { verifyProbeUserCalled() verifyGenerateVerificationCodeCalled() } sut.verifyLoginModelStateIs(State.EMAIL_VERIFICATION) } unit test in SWEETEST
  13. class AuthManagerSteps(testContext: TestContext) : BaseSteps(testContext, appModuleTestingConfiguration) { override fun configure()

    = super.configure() .onSetUp(this::setUp) private val instance by dependency<AuthManager>() private val user by steps<UserSteps>() private fun setUp() { if (instance.isMock) { `when`(instance.loginOrRegister(anyString(), anyString())).then { val password = it.arguments[1] as String if (user.isPasswordCorrect(password)) { val email = it.arguments[0] as String if (user.isUserExisting(email)) { AuthManager.LoginOrRegisterResult.LOGGED_IN } else { AuthManager.LoginOrRegisterResult.REGISTERED } } else { throw AuthManager.WrongPasswordException() } step definitions in SWEETEST user.exists = false
  14. fun whenLoggingInOrRegistering() { instance.loginOrRegister(user.email, user.password) } fun whenLoggingOut() { instance.logout()

    } fun thenLoginOrRegisterIsCalled() { verify(instance).loginOrRegister(user.email, user.password) } fun thenLoginOrRegisterThrows() { assertNotNull(loginOrRegisterThrowable) } step definitions in SWEETEST
  15. AuthManagerSteps Mock Assert LoginVM AuthManager BackendGW LoginVMSteps Setup Mock Interact

    Assert LoginVMTest Setup Mock Interact Assert BackendGWSteps Mock Assert Setup Interact LoginIntegrationTest Setup Mock Interact Assert
  16. class LoginViewModelTest : BaseJUnitTest(appModuleTestingConfiguration) { override fun configure() = super.configure()

    .requireReal<LoginViewModel>() private val sut by steps<LoginViewModelSteps>() private val navigator by steps<LoginNavigatorSteps>() private val loginService by steps<LoginServiceSteps>() private val user by steps<UserSteps>() private val features by steps<FeaturesSteps>() private val connectivity by steps<ConnectivitySteps>() unit test in SWEETEST
  17. class LoginViewModelTest : BaseJUnitTest(appModuleTestingConfiguration) { override fun configure() = super.configure()

    .requireReal<LoginViewModel>() .requireReal<LoginService>() private val sut by steps<LoginViewModelSteps>() private val navigator by steps<LoginNavigatorSteps>() private val loginService by steps<LoginServiceSteps>() private val user by steps<UserSteps>() private val features by steps<FeaturesSteps>() private val connectivity by steps<ConnectivitySteps>() integration test in SWEETEST
  18. val appModuleTestingConfiguration = moduleTestingConfiguration { dependency mockOnly of<SessionStore>() dependency mockOnly

    of<BackendGateway>() dependency any of<AuthManager>() dependency realOnly initializer { LoginViewModel(instanceOf()) } } configuration in SWEETEST
  19. @Before fun before() { syncCoordinator = SyncCoordinatorImpl(mutableListOf(), mock<ConnectivityUtil>(), TestSchedulerProvi user

    = User().apply { emailAddress = EMAIL } loginService = LoginService(mock<ClientInfo>(), syncCoordinator, mock<UserSettings>(), mock<UserPreferences>(), eventBus, httpService, mock<BackendUrlStore>(), authStringGenerator) Tracker.init(mock<TrackerEngine>()) sut = LoginViewModel(scheduler, mock<Logger>(), loginService, resourceProvider).apply { bind(navigator, null, null) userInput.emailAddress = EMAIL userInput.password = PASSWORD } // [...] MESS
  20. Feature: Login, registration and logout Background: Given there is a

    user existing with email address "[email protected]" and password "secure1" Scenario: User exists When trying to login or register with email address "[email protected]" and password "secure1" Then the user "[email protected]" is logged in as an existing user Scenario: Existing user, incorrect password When trying to login or register with email address "[email protected]" and password "wrongpass" Then the user can't enter the app Scenario Outline: Invalid email address When trying to login or register with email address "<email>" Then the user can't enter the app Examples: | email | | [email protected] | | wrong.net | | @sth.org | BDD example
  21. @Given("^there is a user existing with email address \"([^\"]*)\" and

    password \"([^\"]*)\"$") fun givenRegisteredUser(email: String, password: String) { existingEmail = email correctPassword = password } BDD example