Testing (kotlin.test)
In this course, we’re going to use the kotlin.test to crete and execute our tests. This package integrates with Gradle as well.
Setup
We will rely on Gradle to install our test framework as a project dependency. Include this line in the dependencies of your build.gradle.kts
configuration file.
dependencies {
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
Concepts
As Agile developers, we have a range of tests that we can generate based on the value that they add. We will focus on technology-facing tests that provide value to development, since that aligns with our interests in this course. In the “real-world”, we would have a broad range of tests covering all of these interests.

Unit tests
are meant to check the smallest units of your code. A unit test
is a test that meets the following requirements (Khorikov 2020):
- Verifies a single unit of behavior (typically a
class
), - Does it quickly, and
- Does it in isolation from other code.
Testing in isolation means removing the effects of any dependencies on other code, libraries or systems. This means that when you test the behaviour of your class, you are assured that nothing else is contributing to the results that you are observing.
Component integration tests
are created when we care about how multiple classes together are used to provide functionality.
Creating Tests
A test is simply a Kotlin class, with annotated methods that tell the compiler to treat the code as a test instead of implementation code.
The convention is to name unit tests after their class they’re testing, with “Test” added as a suffix. In this example, we’re creating a test for a Model
class so the test is automatically named ModelTest
.

src/test
is used for tests that can run on your development machine, and src/androidTest
is used for tests that need to run on an Android device (e.g. the AVD where you are testing).The tests that you generate should:
- be completely self-contained,
- create instances of the classes that they want to test, and
- check the results of each function to confirm that they meet expected behaviour (by using
assertions
).
For example, here’s a unit test that checks that the Model class can add an Observer properly. The addObserver()
method is the test (you can tell because it’s annotated with @Test
). The @Before
method runs before the test, to setup the environment. We could also have an @After
method if needed to tear down any test structures.
class ObserverTests() {
lateinit var model: Model
lateinit var view: IView
class MockView() : IView {
override fun update() {
}
}
@Before
fun setup() {
model = Model()
view = MockView()
}
@Test
fun addObserver() {
val old = model.observers.count()
model.addObserver(view)
assertEquals(old+1, model.observers.count())
}
}
Annotations
The kotlin.test
package provides additional annotations to denote how tests are managed:
Annotation | Purpose |
---|---|
@Test | Marks a function as a test to be executed |
@BeforeTest | Marks a function to be invoked before each test |
@AfterTest | Marks a function to be invoked after each test |
@Ignore | Mark a function to be ignored |
@Test | Marks a function as a test |
Assertions
There are additional assertions that can be useful.
Function | Purpose |
---|---|
assertEquals | Provided value matches the actual value |
assertNotEquals | The provided and actual values do not match |
assertFalse | The given block returns false |
assertTrue | The given block returns true |
Running Tests
When building your project, Gradle will automatically execute any tests that are placed in this directory structure. There is also a Gradle test
task that will just execute and report test results.
Additionally, you can run them from within IntelliJ IDEA.

Mocks (Test Doubles)
To achieve isolation in testing, we often create test doubles — classes that are meant to look like a dependent class, but that don’t actually implement all of the underlying behaviour. This lets us swap in these “fake” classes for testing.
Mocks are “objects pre-programmed with expectations which form a specification of the calls they are expected to receive.” - Martin Fowler, 2006.
A mock is meant to behave like the “actual” implementation class. For example, we can have a mocked filesystem that would report a file as saved, but would not actually modify the underlying file structure.
Why do we even use mocks?
- They let us test an interface without actually implementing it. e.g., we can tests our domain objects as-if we had a database.
- Since we’re not interacting with “real” systems, we avoid any risk of working with “live” data during testing.
You can easily create these mock classes yourself for code domain objects. Several libraries have also been established to help create mocks of objects. Mockito
is one of the most famous, and it can be complemented with Mockito-Kotlin. Here’s an example of a mock file that reports a path, but doesn’t actually do anything else.
private val mockedFile: File {
return mock { on { absolutePath} doReturn "/random"}
}
Dependency Injection
Dependency injection is the practice of supplying dependencies to an object in it’s argument list instead of allowing the object to create them itself. If you design your classes this way, then it’s easy to create an instance of a mock and provide that to your function, instead of allowing the function to instantiate the object itself.
You do this by:
- Introducing interfaces for all external dependencies, and
- Using DI to describe the relationship.
For example, we can mock a DB repository using this technique.
data class User(firstName: String, lastName: String)
enum class Status { OK, ERROR }
// every repo uses this interface
interface IRepository {
fun save(user: User): Status
}
// actual repo returns result of the operation
class UserRepository : IRepository {
private connection = DB.connect(...)
override fun save(user: User): Status {
return connection.save(user)
}
}
// fake repo always returns OK
class MockRepository: IRepository {
overide fun save(user: User) {
return Status.OK
}
}
@Test
fun checkConnection() {
val repo = MockRepository()
val user = User("Sam", "Spade")
val status = repo.save(user)
assert(status, Status.OK)
}
This is common when testing, even in cases when that class may represent the only realization of an interface. This allows you to easily write mocks against the interface, where it’s relatively easy to determine what expected behaviour should be.
Last Word
