5
Unit Tests
Written by Fernando Sproviero
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
As mentioned in Chapter 4, “The Testing Pyramid,” unit tests verify how isolated parts of your application work. Before checking how things work together, you need to make sure the units of your application behave as expected.
In this chapter, you’ll:
- Learn what unit tests are and what are the best places to use them.
- Write unit tests using the test-driven development (TDD) pattern to learn these concepts in the context of TDD.
Throughout this chapter and Chapter 7, “Introduction to Mockito” you’ll work on an application named Cocktail Game. With this application, you’ll have fun with a trivia game about cocktails.
Find the starter project for this application in the materials for this chapter and open it in Android Studio. Build and run the application. You’ll see a blank screen.
You’ll start writing tests and classes for the application and, by the end of Chapter 7, “Introduction to Mockito,” the application will look like this:
When to use unit tests
Unit tests are the fastest and easiest tests to write. They also are the quickest to run. When you want to ensure that a class or method is working as intended in isolation — this means with no other dependent classes — you write unit tests.
Before writing any feature code, you should first write a unit test for one of the classes that will compose your feature. Afterwards, you write the class that will pass the test. After repeating this procedure, you’ll have a completed, testable feature.
Setting up JUnit
You’re going to write a unit test for the first class of the cocktail game, which is a Game
class. This first test will be a JUnit test, so, open app/build.gradle and add the following dependency:
dependencies {
...
testImplementation 'junit:junit:4.12'
}
Creating unit tests
To start, switch to the Project View and open app ‣ src. Create a new directory and enter: test/java/com/raywenderlich/android/cocktails/game/model. Then, create a file called GameUnitTests.kt.
class GameUnitTests {
// 1
@Test
fun whenIncrementingScore_shouldIncrementCurrentScore() {
// 2
val game = Game()
// 3
game.incrementScore()
// 4
Assert.assertEquals(1, game.currentScore)
}
}
Assert.assertEquals("Current score should have been 1",
1, game.currentScore)
Making the test compile
The test won’t compile because the Game
class doesn’t exist. So, create the Game
class under the directory app ‣ src ‣ main ‣ java ‣ com ‣ raywenderlich ‣ android ‣ cocktails ‣ game ‣ model. You’ll need to create game
and model
packages first. In the Game
class, write the minimum amount of code to make the test compile:
class Game() {
var currentScore = 0
private set
fun incrementScore() {
// No implementation yet
}
}
Running the test
Now, go back to the test; you’ll see that it compiles.
Making the test pass
Modify the Game
class to make it pass:
class Game() {
var currentScore = 0
private set
fun incrementScore() {
currentScore++
}
}
Creating more tests
The game will show a highest score. So, you should add a test that checks that when the current score is above the highest score, it increments the highest score:
@Test
fun whenIncrementingScore_aboveHighScore_shouldAlsoIncrementHighScore() {
val game = Game()
game.incrementScore()
Assert.assertEquals(1, game.highestScore)
}
var highestScore = 0
private set
fun incrementScore() {
currentScore++
highestScore++
}
@Test
fun whenIncrementingScore_belowHighScore_shouldNotIncrementHighScore() {
val game = Game(10)
game.incrementScore()
Assert.assertEquals(10, game.highestScore)
}
class Game(highest: Int = 0) {
var highestScore = highest
private set
fun incrementScore() {
currentScore++
if (currentScore > highestScore) {
highestScore = currentScore
}
}
JUnit annotations
For this project, you’re creating a trivia game. Trivias have questions, so you’ll now create unit tests that model a question with two possible answers. The question also has an “answered” option to model what the user has answered to the question. Create a file called QuestionUnitTests.kt in the app ‣ src ‣ test ‣ java ‣ com ‣ raywenderlich ‣ android ‣ cocktails ‣ game ‣ model directory.
class QuestionUnitTests {
@Test
fun whenCreatingQuestion_shouldNotHaveAnsweredOption() {
val question = Question("CORRECT", "INCORRECT")
Assert.assertNull(question.answeredOption)
}
}
class Question(val correctOption: String,
val incorrectOption: String) {
var answeredOption: String? = "MY ANSWER"
private set
}
class Question(val correctOption: String,
val incorrectOption: String) {
var answeredOption: String? = null
private set
}
@Test
fun whenAnswering_shouldHaveAnsweredOption() {
val question = Question("CORRECT", "INCORRECT")
question.answer("INCORRECT")
Assert.assertEquals("INCORRECT", question.answeredOption)
}
fun answer(option: String) {
// No implementation yet
}
fun answer(option: String) {
answeredOption = option
}
@Test
fun whenAnswering_withCorrectOption_shouldReturnTrue() {
val question = Question("CORRECT", "INCORRECT")
val result = question.answer("CORRECT")
Assert.assertTrue(result)
}
fun answer(option: String): Boolean {
answeredOption = option
return false
}
fun answer(option: String): Boolean {
answeredOption = option
return true
}
@Test
fun whenAnswering_withIncorrectOption_shouldReturnFalse() {
val question = Question("CORRECT", "INCORRECT")
val result = question.answer("INCORRECT")
Assert.assertFalse(result)
}
fun answer(option: String): Boolean {
answeredOption = option
return correctOption == answeredOption
}
@Test(expected = IllegalArgumentException::class)
fun whenAnswering_withInvalidOption_shouldThrowException() {
val question = Question("CORRECT", "INCORRECT")
question.answer("INVALID")
}
fun answer(option: String): Boolean {
if (option != correctOption && option != incorrectOption)
throw IllegalArgumentException("Not a valid option")
answeredOption = option
return correctOption == answeredOption
}
val isAnsweredCorrectly: Boolean
get() = correctOption == answeredOption
fun answer(option: String): Boolean {
if (option != correctOption && option != incorrectOption)
throw IllegalArgumentException("Not a valid option")
answeredOption = option
return isAnsweredCorrectly
}
Refactoring the unit tests
Notice that each test repeats this line of code:
val question = Question("CORRECT", "INCORRECT")
private lateinit var question: Question
@Before
fun setup() {
question = Question("CORRECT", "INCORRECT")
}
@Test
fun whenCreatingQuestion_shouldNotHaveAnsweredOption() {
Assert.assertNull(question.answeredOption)
}
@Test
fun whenAnswering_shouldHaveAnsweredOption() {
question.answer("INCORRECT")
Assert.assertEquals("INCORRECT", question.answeredOption)
}
@Test
fun whenAnswering_withCorrectOption_shouldReturnTrue() {
val result = question.answer("CORRECT")
Assert.assertTrue(result)
}
@Test
fun whenAnswering_withIncorrectOption_shouldReturnFalse() {
val result = question.answer("INCORRECT")
Assert.assertFalse(result)
}
@Test(expected = IllegalArgumentException::class)
fun whenAnswering_withInvalidOption_shouldThrowException() {
question.answer("INVALID")
}
Challenge
Challenge: Testing questions
You have the Game
and Question
classes. The Game
class should contain a list of questions. For now, these are the requirements:
Key points
- Unit tests verify how isolated parts of your application work.
- Using JUnit, you can write unit tests asserting results, meaning, you can compare an expected result with the actual one.
- Every test has three phases: set up, assertion and teardown.
- In TDD, you start by writing a test. You then write the code to make the test compile. Next you see that the test fails. Finally, you add the implementation to the method under test to make it pass.
Where to go from here?
Great! You’ve just learned the basics of unit testing with JUnit. You can check the project materials for the final version of the code for this chapter.