The problem

Let's suppose that we have to write tests for presenter or viewmodel that supports two paths: anonymous and logged in user. We have use cases telling us if user is currently logged in, use case for fetching feed which we will present to the user and router for handling some of navigation events.

Our flow

For logged in user, on start:

  • we have to fetch feed for that user
  • we have to subscribe to notifications
  • we have to set users avatar
  • on settings icon click, we have to open setting screen

But for anonymous user:

  • we fetch some generic feed "most popular"
  • we don't subscribe to notifications
  • we set default avatar
  • on settings icon click, we open login / registration screen

Presenter dependencies are described by the following interfaces:

interface Router {
    fun openLogin()
    fun openSettings()
}

fun interface ResolveUserUseCase {
    fun execute(): CurrentUser

    sealed class CurrentUser {
        object Anonymous : CurrentUser()
        data class LoggedIn(val userId: String) : CurrentUser()
    }
}

fun interface FetchFeedUseCase {
    fun fetchFeed(kind: FeedRequestKind): List<FeedItem>

    sealed class FeedRequestKind {
        object MostPopular : FeedRequestKind()
        data class UserFeed(val userId: String) : FeedRequestKind()
    }
}

interface View {
    fun setAvatar(avatarUrl: String? = null)
    fun setFeed(list: List<FeedItem>)
}

fun interface SubscribeToNotificationsUseCase {
    fun execute()
}

data class FeedItem(val content: String)
System Under Test dependencies

We have several test cases that slightly differs for logged in and anonymous flows:

And presenter will look like this:

class Presenter(
    private val view: View,
    private val router: Router,
    private val resolveUserUseCase: ResolveUserUseCase,
    private val fetchFeedUseCase: FetchFeedUseCase,
    private val subscribeToNotificationsUseCase: SubscribeToNotificationsUseCase
) {
    fun start() {
        // do things
    }
    
    fun settingsClick(){
        // open proper screen
    }
}
System Under Test

First tests

Eventually when we will start implementing tests in given-when-then manner they may look like this:

import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

class PresenterTest {

    @Test
    @DisplayName("given user is logged in when start then set avatar")
    fun loggedInSetAvatar() {
        // ... arrange
        val resolveUserUseCase = ResolveUserUseCase { LoggedIn(userId = "asd123") }
        // ... act
        // ... assert
    }

    @Test
    @DisplayName("given user is logged in when start then subscribe for notifications")
    fun loggedInSubscribeNotifications() {
        val resolveUserUseCase = ResolveUserUseCase { LoggedIn(userId = "asd123") }
    }

    @Test
    @DisplayName("given user is logged in when start then fetch user feed")
    fun loggedInFetchFeed() {
        val resolveUserUseCase = ResolveUserUseCase { LoggedIn(userId = "asd123") }
    }

    @Test
    @DisplayName("given user is logged in when click settings then open settings view")
    fun loggedInOnSettingsClickOpenSettings() {
        val resolveUserUseCase = ResolveUserUseCase { LoggedIn(userId = "asd123") }
    }

    @Test
    @DisplayName("given anonymous user when start then set default avatar")
    fun anonymousSetAvatarDefault() {
        val resolveUserUseCase = ResolveUserUseCase { Anonymous }
    }

    @Test
    @DisplayName("given anonymous user when start then don't subscribe for notifications")
    fun anonymousDontSubscribeToNotifications() {
        val resolveUserUseCase = ResolveUserUseCase { Anonymous }
    }

    @Test
    @DisplayName("given anonymous user when start then fetch most popular feed")
    fun anonymousFetchPopularFeed() {
        val resolveUserUseCase = ResolveUserUseCase { Anonymous }
    }

    @Test
    @DisplayName("given anonymous user when click settings then open login screen")
    fun anonymousOnSettingsClickOpenLogin() {
        val resolveUserUseCase = ResolveUserUseCase { Anonymous }
    }
}
A lot of repetitions - what if we configure stub for ResolveUserUseCase once per flow?
8 tests for presenter, in given-when-then manner

The solution

There is a way to refactor those tests without too much modification. We will use @Nested annotation and inner class.

import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test

class PresenterNestedTest {

    @Nested
    @DisplayName("given user logged in")
    inner class UserLoggedIn {
        val resolveUserUseCase = ResolveUserUseCase { LoggedIn(userId = "asd123") }

        @Test
        @DisplayName("when start then set avatar")
        fun setAvatar() {}

        @Test
        @DisplayName("when start then subscribe for notifications")
        fun subscribeNotifications() {}

        @Test
        @DisplayName("when start then fetch user feed")
        fun fetchFeed() {}

        @Test
        @DisplayName("when click settings then open settings view")
        fun onSettingsClickOpenSettings() {}
    }

    @Nested
    @DisplayName("given anonymous user")
    inner class AnonymousUser {

        val resolveUserUseCase = ResolveUserUseCase { Anonymous }

        @Test
        @DisplayName("when start then set default avatar")
        fun setDefaultAvatar() {}

        @Test
        @DisplayName("when start then don't subscribe for notifications")
        fun dontSubscribeToNotifications() {}

        @Test
        @DisplayName("when start then fetch most popular feed")
        fun fetchPopularFeed() {}

        @Test
        @DisplayName("when click settings then open login screen")
        fun onSettingsClickOpenLogin() {}
    }
}
Reusing test double across test groups
They are grouped in IntelliJ display!

So what we did here?

  1. Selected two paths: logged in and anonymous
  2. Created two inner classes with @Nested annotation in test class
  3. Created stub for ResolveUserUseCase once per nested test
  4. Refactored test names a little bit, so we wouldn't have given twice rendered in test result
If you want to make use of nested tests in your project, make sure that you are actually using Junit5 instead of Junit4.

Summary

Nesting test in Junit5 is interesting feature, it makes easier following Don't Repeat Yourself principle, since we have more convenient of for sharing test doubles between common test cases.

While we are refactoring tests to make use of nesting mechanism, we may come up with some refactoring ideas - maybe some parts of system under test that are now covered with @Nested mechanism deserves extraction to separate class?

It is also some way of introducing BDD style into test suite - in nested groups we can define tests for common behaviors in more readable way.

And last but not least - IDE support for nested tests really speaks to me. One thing is displaying test result, and the other - possibility of running only part of test class.

JUnit 5 User Guide
.