Skip to content

Testing components

Maxim Kostenko edited this page Jun 21, 2022 · 2 revisions

One of the main goals of a good architecture is to enforce its user to write a good and testable code. With the Kompot framework, you can easily test each component of your screen or flow.

Testing screen

ScreenModel

We will test InputScreenModel created on the screen components page. It is a model of a simple screen that accepts users input and have a "Continue" button to confirm this input and go to the next screen.

As we learned in the previous sections, the screen model responds to the user's interaction with the screen and calculates a DomainState that is the only source of UI updates.

Therefore all we need to do to make proper testing of the screen model is to invoke its public methods and lifecycle events and then assert the DomainState produced.

InputScreenModel has an initial state with the blank input because the user didn't do any typing yet. The state also includes the type of input.

Domain state structure:

data class DomainState(
    val inputType: InputType,
    val inputText: String
) : ScreenStates.Domain

Test for the initial state:

@Test
fun `should have initial state with input type and blank input`() = dispatchBlockingTest {
    val screenModel = createScreenModel(InputType.FIRST_NAME)

    val streamTest = screenModel.domainStateStream().testIn(this)

    streamTest.assertValues(
        DomainState(InputType.FIRST_NAME, "")
    )
}

After seeing the initial state, user can start typing, so we test what happens after the screen model receives an input event:

@Test
fun `should update state with new input`() = dispatchBlockingTest {
    val screenModel = createScreenModel(InputType.FIRST_NAME)

    val input = "hello"

    val streamTest = screenModel.domainStateStream().testIn(this)

    screenModel.onInputChanged(input)

    streamTest.assertValues(
        listOf(
            DomainState(
                inputType = InputType.FIRST_NAME,
                inputText = ""
            ),
            DomainState(
                inputType = InputType.FIRST_NAME,
                inputText = input
            )
        )
    )
}

Finally, user confirms the input and screen model prepares an output with the text typed:

@Test
fun `should return result with the latest input when action clicked`() = dispatchBlockingTest {
    val screenModel = createScreenModel()

    val input = "hello"

    val streamTest = screenModel.resultStream().testIn(this)

    screenModel.onInputChanged(input)
    screenModel.onActionClick()

    streamTest.assertValues(OutputData(input))
}

Full code of InputScreenModelTest can be found here.

Screen StateMapper

State mapper is responsible for mapping the domain state to UI state. It has only one method with a single parameter and UI State as an output:

fun mapState(domainState: DomainState): UIState

Therefore testing an output against different domain states would be sufficient for a proper state mapper coverage:

class InputStateMapperTest {

    private val stateMapper = InputStateMapper()

    @Test
    fun `should map state for name input`() {
        val domainState = DomainState(inputType = InputType.FIRST_NAME, inputText = "John")

        val expected = UIState(inputHint = "Input first name", inputText = "John")
        assertEquals(expected, stateMapper.mapState(domainState))
    }

    @Test
    fun `should map state for surname input`() {
        val domainState = DomainState(inputType = InputType.LAST_NAME, inputText = "Newman")

        val expected = UIState(inputHint = "Input last name", inputText = "Newman")
        assertEquals(expected, stateMapper.mapState(domainState))
    }

}

Testing flow

Flow is responsible for showing a sequence of screens. In this introduction, we'll test the improved version of the AddContactFlow previously described here.

The flow of adding a contact shows two screens, one for the first name input and the other one for the last name. Flow finishes with saving new contact by calling saveContact(contact: Contact) method of ContactsRepository.

Each screen of the flow corresponds to a predefined step. When the screen returns a result, flow decides which step is the next and shows a new screen. To test flow behaviour, we simply need to verify that all the steps are called in the correct sequence with the help of FlowModelAssertion API:

@Test
fun `should create and save contact`() = dispatchBlockingTest {
    val firstName = "John"
    val lastName = "Doe"

    val expectedContact = Contact(
        firstName = firstName,
        lastName = lastName
    )

    flowModel.test()
        .assertStep(
            step = Step.InputFirstName,
            result = InputScreenContract.OutputData(firstName)
        )
        .assertStep(
            step = Step.InputLastName,
            result = InputScreenContract.OutputData(lastName)
        ).also {
            verify(contactsRepository).saveContact(expectedContact)
        }
        .assertQuitFlow()
}

FlowModelAssertion gives you all the tools needed to test the main use cases of the flow: screens navigation, back press, calling modal screens/flows, returning a result and so on. Feel free to explore assertions to find one that fit your needs.

Full code for AddContactFlow test can be found here.