Welcome to the very last lesson of the "Increasing Code Test Coverage" course! In our journey so far, we've explored the importance of code coverage, the role of characterization tests, and how traits and mocks can enhance testability. Now, we'll bring these concepts together to achieve comprehensive code coverage by integrating test writing and mocking techniques. This lesson will help you solidify your understanding and prepare you for practical application.
Let's examine an implementation of a user processor. We want to add tests to describe the characteristics of the system. We also don't want our tests to depend on a database. Let's take a closer look at the implementation of the UserProcessor
class:
Scala1import java.time.Instant 2import scala.util.Try 3 4class UserProcessor(userDatabase: UserDatabase = UserDatabase()): 5 def updateUserLoginDate(userId: Int): Boolean = 6 Try: 7 userDatabase.getUserById(userId).fold(false) { user => 8 val updatedUser = user.copy(lastLoginDate = Instant.now()) 9 userDatabase.updateUser(updatedUser) 10 true 11 } 12 .getOrElse(false) 13
The class uses a UserDatabase
class as a dependency. This class can be viewed below:
Scala1import java.time.Instant 2 3case class User(id: Int, name: String, email: String, lastLoginDate: Instant) 4 5class UserDatabase: 6 def getUserById(userId: Int): Option[User] = 7 println("[Database Operation] Should not happen in tests!") 8 Some(User(userId, "Default", "default@example.com", Instant.now())) 9 10 def updateUser(user: User): Unit = 11 println("[Database Operation] Should not happen in tests!")
First, we will focus on adding a trait for the database layer to enable us to mock this particular dependency. After that, we will focus on writing a collection of tests that will describe the system and help us increase code test coverage for the UserProcessor
class.
When writing tests with mocks, consider the following best practices:
- Focus on Behavior: Ensure tests verify the behavior of the code, not the implementation details.
- Use Descriptive Names: Name tests clearly to indicate the scenario being tested.
- Verify Interactions: Use mock verifications to ensure the correct methods are called.
- Handle Exceptions: Test how the code handles exceptions to ensure robustness.
By following these practices, you can create reliable and maintainable tests that provide confidence in your code.
As you move on to the practice exercises, you'll have the opportunity to apply all the things you have learned so far in this course. Good luck, and enjoy the journey of mastering code test coverage!
