Unit Testing
What is a unit test?
Let’s define it more formally. A unit test is a test that meets the following three requirements [Khorikov 2020]:
- Verifies a single unit of behavior,
- Does it quickly, and
- Does it in isolation from other tests.
Unit tests should target classes or components in your program. i.e. they should exercise how a particular class works. They should be small, very focused, and quick to execute and return results.
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. We’ll discuss strategies to accomplish this in the [Dependencies] section.
This is also why designing cohesive, loosely coupled classes is critical: it makes testing them so much easier if they work independently!
Structuring a test
A typical unit test uses a very particular pattern, known as the arrange, act, assert
pattern. This suggests that each unit test should consist of these three parts:
- Arrange: bring the system under test (SUT) to the starting state.
- Act: call the method or methods that you want to test.
- Assert: verify the outcome of the above action. This can be based on return values, or some other conditions that you can check.
Note that your Act section should have a single action, reflecting that it’s testing a single behaviour. If you have multiple actions taking place, that’s a sign that this is probably an integration test (see below). As much as possible, you want to ensure that you are writing minimal-scope unit tests.
Another anti-pattern is an if
statement in a test. If you are branching, it means that you are testing multi-ple things, and you should really consider breaking that one test into multiple tests instead.
How to write tests
Practically, a unit test is just a Kotlin class with annotated methods that tell the compiler how to treat the code. It’s best practice to have one test class for each implementation class that you want to test. e.g. class Main
with a test class MainTest
. This test class would contain multiple methods, each representing a single unit test.
Tests should be placed in a special test folder in the Gradle directory structure. When building, Gradle will automatically execute any tests that are placed in this directory structure1.
Your unit tests should be structured to create instances of the classes that they want to test, and check the results of each function to confirm that they meet expected behaviour.
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())
}
}
Let’s walkthrough creating a test.
To do this in IntelliJ, select a class in the editor, press Alt-Enter
, and select “Create Test” from the popup menu. This will create a unit test using JUnit, the default test framwork. There is a detailed walkthrough on the IntelliJ support site.
You can also just create the test files by-hand. Just make sure to save them in the correct location!
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
. This is just a convention - you can name it anything that you want.
We’ve manually added the addition()
function. We can add as many functions as we want within this class. By convention, they should do something useful with that particular class.
Below, class ModelTest
serves as container for our test functions. In it, we have two unit tests that will be automatically executed when we built. NOTE that you would need more than these two tests to adequately test this class - this is just an example.
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class ModelTests {
lateinit var model: Model
@Before
fun setup() {
model = Model()
model.counter = 10
}
@Test
fun checkAddition() {
val original = model.counter
model.counter++
assertEquals(original+1, model.counter)
}
@Test
fun checkSubtraction() {
val original = model.counter
model.counter--
assertEquals(original-1, model.counter)
}
@After
fun teardown() {
}
}
The kotlin.test
package provides annotations to mark test functions, and denote how they 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 |
In our test, we call utility functions to perform assertions of how the function should successfully perform.
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 |
How to run tests
Tests will be run automatically with gradle build
or we can execute gradle test
to just execute the tests.
$ gradle test
BUILD SUCCESSFUL in 760ms
3 actionable tasks: 3 up-to-date
Gradle will report the test results, including which tests - if any - have failed.
You can also click on the arrow beside the test class or name in the editor. For example, clicking on the arrow in the gutter would run this addObserver()
test.
-
Android has two test folders:
src/test
is used for tests that can run on your development machine, andsrc/androidTest
is used for tests that need to run on an Android device (e.g. the AVD where you are testing). ↩︎