Blog Infos
Author
Published
Topics
, , , ,
Published
Photo by Marek Piwnicki on Unsplash

 

This is a series of articles about how to architecture your app that it was inspired by Google Guide to App Architecture and my personal experience.

In previous articles, I’ve covered the DataDomain, and Presentation layers of the app, and one of the reasons why I architected this layer in that way is to build a testable code base. It brings the code structure that allows you to easily test different parts of it in isolation. Testable architectures have other advantages, such as better readability, maintainability, scalability, and reusability.

Today I want to give you a few recommendations on how to cover components, from these layers, with tests. There are different types of tests such as Unit, End-to-end, and Integration tests. In this article, I’m aiming to cover only Unit tests as it’s usually around 80% of all tests on the project.

Testing your app is an integral part of the app development process. By running tests against your app consistently, you can verify your app’s correctness, functional behavior, and usability before you release it publicly.

Testing also offers the following advantages:

  • Early failure detection in the development cycle.
  • Safer code refactoring, allows you to optimize code without worrying about regressions.
  • Stable development velocity, helping you minimize technical debt.
Testing Edge Cases

Unit tests should focus on both normal and edge cases. Edge cases are uncommon scenarios that human testers and larger tests are unlikely to catch. For example — corrupted data, network connection errors, etc.

Unit Tests to Avoid

Some unit tests should be avoided because of their low value. The golden rule is not to cover a framework or library components and interactions with them that do not contain business logic.

Components to cover
Data layer
  • Unit tests for the data layer, especially repositories. Most of the data layer should be platform-independent. Doing so enables test mocks to replace database modules and remote data sources in tests.
  • Unit tests for the DataSource if you have some logic in it, such as exception mapping.
  • Unit tests for the mapping DTO to Domain models and vice versa.
Domain layer

The Domain layer doesn’t know about any platform dependencies, it should be covered with JUnit tests only.

  • Unit tests for Use Cases as a main component that contains business logic.

If you consider creating an interface for the use case to make it easier to test, don’t. Most of the time it’s overengineering and brings no value unless you have more than one implementation of the same use case.

Presentation layer
  • Unit tests for ViewModels.
  • Unit tests for Navigator.
  • Unit tests for the mapping Domain to UI models and vice versa.
The Given-When-Then Pattern

I recommend following the Given-When-Then (GWT) pattern given by Martin Fowler to write the Unit test.

  • The given part describes the state of the world before you begin the behavior you’re specifying in this scenario. You can think of it as the pre-conditions to the test.
  • The when a section is the behavior that you’re specifying.
  • Finally, the then section describes the changes you expect due to the specified behavior.
Fake data

Often we need fake data for testing and a nice library to create fake data for tests is Faker. It’s useful when you’re developing a new project and need some pretty data for showcase.

val faker: Faker = Faker()
val name = faker.name().fullName() // Miss Samanta Schmidt
val firstName = faker.name().firstName() // Emory
val lastName = faker.name().lastName() // Barton
val streetAddress = faker.address().streetAddress() // 60018 Sawayn Brooks Suite 449
view raw Faker.kt hosted with ❤ by GitHub
ObjectMother pattern

The nice way to organize fake data is to use the ObjectMother pattern. It’s a simple Kotlin file with methods for getting fake data.

Naming conventions

The files are named after the data type that they’re responsible for. The convention is as follows:

type of data + Mother.

For example: CategoryMother.

The methods are named after the data type that they’re responsible for. The convention is as follows:

random + type of data.

For example: randomCategory.

fun randomCategory(children: List<Category> = emptyList()) = Category(
categoryId = CategoryId(faker.number().randomDigitNotZero().toLong()),
postingType = Category.PostingType.DEFAULT,
feedType = Category.FeedType.DEFAULT,
panelType = Category.PanelType.CATEGORY,
adsCount = faker.number().randomDigitNotZero(),
children = children,
name = faker.name().name(),
iconImage = faker.name().fullName(),
image = null,
searchNames = emptyList()
)
Tools

For mocking and verifying in the Junit test, I recommend using io.mockk lib. For making assertions you can choose the lib you want, I recommend using JUnit 5 and the API it provides.

Names for test methods

In tests, I recommend following method names with spaces enclosed in backticks.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Intro to unit testing coroutines with Kotest & MockK

In this workshop, you’ll learn how to test coroutines effectively using the Kotest and MockK libraries, ensuring your app handles concurrent tasks efficiently and with confidence.
Watch Video

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michalik
Kotlin GDE

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michalik
Kotlin GDE

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michali ...
Kotlin GDE

Jobs

@Test
fun `On invoke should return correct list of categories`() = runTest {
}
view raw TestNaming.kt hosted with ❤ by GitHub

Finally, let’s look at the test example of the Repository.

class CategoriesDataRepositoryTest {
private val apiManager: APIManager = mockk(relaxed = true)
private val categoriesInMemoryDataSource: CategoriesInMemoryDataSource = mockk(relaxed = true)
private val categoriesLocalDataSource: CategoriesLocalDataSource = mockk(relaxed = true)
private val categoriesDataRepository = CategoriesDataRepository(
apiManager = apiManager,
categoriesInMemoryDataSource = categoriesInMemoryDataSource,
categoriesLocalDataSource = categoriesLocalDataSource,
)
@Test
fun `On get new root instance should return correct result`() = runTest {
// Given
val categoryName = faker.name().name()
val children: List<Category> = listOf(
randomCategory(),
randomCategory(),
randomCategory()
)
// When
val result = categoriesDataRepository.getNewRootInstance(
name = categoryName,
children = children
)
// Then
assertEquals(Category.ROOT, result.categoryId)
assertEquals(categoryName, result.name)
assertEquals(children.size, result.adsCount)
assertTrue { result.searchNames.isEmpty() }
assertNull(result.image)
assertNull(result.iconImage)
assertEquals(Category.PostingType.DEFAULT, result.postingType)
assertEquals(Category.FeedType.DEFAULT, result.feedType)
assertEquals(Category.PanelType.CATEGORY, result.panelType)
assertFalse(result.categoryId.isDuplicate)
}
}

Example of Use Case test:

class GetCategoriesFromIdsUseCaseTest {
private val categoriesRepository: CategoriesRepository = mockk(relaxed = true)
private val getCategoriesFromIdsUseCase = GetCategoriesFromIdsUseCase(
categoriesRepository = categoriesRepository
)
@Test
fun `On invoke should return correct list of categories`() = runTest {
// Given
val child1CategoryId = Category.CATEGORY_PRO
val child1: Category = mockk {
every { categoryId } returns child1CategoryId
}
val child2CategoryId = CategoryId(id = -faker.number().randomNumber())
val child2: Category = mockk {
every { categoryId } returns child2CategoryId
}
val categoryTreeIds: List<CategoryId> = listOf(
child1CategoryId,
child2CategoryId,
)
val map = mapOf(
child1CategoryId to child1,
child2CategoryId to child2,
)
coEvery { categoriesRepository.getCategories() } returns map
// When
val result = getCategoriesFromIdsUseCase(categoryTreeIds)
// Then
assertContains(result, child1)
assertContains(result, child2)
coVerify { categoriesRepository.getCategories() }
}
}
Wrapping up

Unless the project is as simple as a Hello World app, you should test it. Good architecture design is a key to making your life with tests easier. Today I gave you a few ideas on how you can organize and improve Unit tests in your projects.

You can find more test examples in the sample project on Moove.

Stay tuned for the next App Architecture topic to cover.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE
blog
The suspension capability is the most essential feature upon which all other Kotlin Coroutines…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu