Testing is hard and not everyone likes to spend their time writing unit tests when they could be building shiny new features.
Almost every developer understands the value of testing their code. However, most of us fall prey to laziness in the face of approaching deadlines and the prospect of more exciting work.
In this post, let’s understand why unit tests serve as the backbone of successful products and learn a new way of writing tests that is much simpler, intuitive and appealing.
Why Testing Matters
Since I mostly work with Android applications, I’ll let you in on a little secret. More than half of the codebases you will come across will have little to no tests. Not just the crappy ones, but sometimes products with thousands of users.
This is often due to the complex and dull nature of old school JUnit based testing. Not only is the process is quite boring, but it also requires a lot of repetitive boilerplate code to be written for even the simplest scenarios, making testing a very time-consuming process.
Obviously, when you are in a rush to take the product out as soon as possible, testing becomes the least prioritized work.
More often than not, the end result is that the project becomes unmaintainable within a year. I have seen organizations rewriting already published products only because the code base has become unmaintainable due to spaghetti code that is riddled with bugs.
Obviously this is not a desirable situation. Testing your code for bugs and architectural flaws from the beginning solidify your product and make it more manageable in the long run.
“If you can not measure it, you can not improve it.” — Lord Kelvin
Testing As You Go
Laying the foundations of your project with extensive testing is not a new idea. Behavior Driven Development has been around for decades.
The practice is to feature specifications (or user stories) into a set of unit tests and then writing code that satisfies those specs via passing tests.
This results in the co-development of both your features and their tests side by side. Coupled with a Continuous Integration solution, this setup almost guarantees a significant reduction in regression bugs over time.
Specification Based Testing In Practice
Cucumber is the most widely used BDD testing framework right now. It provides a plain language parser called Gherkin which can be used to write tests in plain English that non-programmers can also understand. The framework turns these specifications into acceptance tests which also serves as the documentation for each feature. Here is what a test is written in Gherkin looks like:
Neat! Isn’t it? This spec results in unit tests that run with Cucumber. Since this post is not about Cucumber, I won’t go into the implementation details. You can visit this tutorial for a full tutorial on BDD style testing with Cucumber. I used the same article to borrow the above example.
Problems with JUnit
Having said that, writing all styles of tests are entirely possible with JUnit. Mockito provides a BDD style extension in its core library that allows you to do similar
Give, When, Then style testing within JUnit test cases.
Let’s take a look at some of the problems associated with it and why I believe vanilla JUnit is not a good combination for a powerful language like Kotlin:
Like everything in Java, JUnit is also quite verbose. You are forced to write too much repetitive code with a slight change of logic in different places to test different but related scenarios.
How many times have you had a test for a HAS and a HAS-NOT condition with all the same contents except for a boolean? For example, a
testUserAccessWhenHasToken and then
Of course, you can stuff in all your assertions in one test at the cost of readability. However, a good test is supposed to be granular i.e. targeting one case per test.
Also, your test code is guaranteed to grow as your project gets bigger and at some point, your test code will surpass your application code. If you want to look at examples of this, try looking into the source code of any popular open-source projects such as RxJava, Retrofit, OkHttp, Picasso, etc. Almost all of them have 1.5x to 2x more test code compared to their business logic.
Lack Of Contextual Information
If you are like me, then one of the first things that you may have done while switching to Kotlin was to change your long JUnit test names like
fun onTouchOutside_shouldDismissDialogAndResumeStreaming() with backtick notation. While a big improvement, it gave us a license to go wild with it in our attempts to provide more contextual information about each test case. So now our test case has evolved into something like this:
fun `on touch outside, dismiss the dialog and resume streaming the paused song` ()
The real issue here is that these names are not enough to provide full context about the test. Unless you are willing to write an entire paragraph to define the pre and post conditions for the scenario.
Another problem here is with the organization of test cases. Most of the test cases have some shared code that can be logically structured into a hierarchy. However, because JUnit only allows you to write tests in the form of class methods. So you end up writing more tests, more code but with less context about the overall theme of the current group of tests.
A Better Way To Write Tests
The reason behind giving you a taste of Cucumber BDD was to show what it would be like to have idiomatic tests.
Let’s build a grade calculator that tells you your grade based on the marks you obtained. Here are the rules for grading:
- When obtained marks are 90 or above, then grade is A.
- When obtained marks are between 80 and 89, then the grade is B
- When obtained marks are between 70 and 79, then the grade is C
- When obtained marks are between 60 and 69, then the grade is D
- When obtained marks are below 60, then the grade is F
First, create an empty Kotlin or Android project and add the following two dependencies:
The first dependency is Kotlintest which is a testing library built on top of JUnit. It takes advantage of Kotlin’s DSL capabilities to support various types of testing styles.
Mockk is a Kotlin-based mocking library with a very clean syntax that blends really well with Kotlintest’s specs. It also provides a much more flexible API and a much wider set of features compared to Mockito or Powermock.
Let’s start by creating an empty
GradeCalculator class and converting our specifications into a Spec.
This does not quite look like our traditional JUnit test. So let’s break it down and understand bit by bit:
- BehaviorSpec: We are extending something called a *BehaviorSpec* which is basically a Spec written in BDD style (remember the
Given, When, Thenfrom Cucumber?). There are dozens of different other Spec styles available in Kotlintest.
- Spyk: You may notice that I wrapped the
GradeCalculatorobject with a
spykmethod. If you have used Mockito before than the concept is the same. Basically, a spy is a wrapper that lets you mock some methods and variables of the object while using the actual values for the rest. I did it to demonstrate the use of Mockk here.
- Every/Returns: Similar to Mockito’s
when/thenstyle, this construct is used by Mockk to prepare mock values. All we are saying is to return a mock value whenever anyone in this code block asks for this value. The value we are mocking is total marks which are going to help calculate the grade.
- When/Then: Finally, there are a bunch of
Thenblocks nested inside
Whenblocks with some description. The
Whenblock is nothing but a way to organize tests in a logical fashion while each
Thenserves as the actual test where the assertion happens. So you can have a test with just a
Thenstatement but then there's no point of using it.
Next, let’s add some functionality to the
Here I added a
totalMarks field which we mock in our test. This value is used in the
getPercentage method to calculate the percentage between total and obtained marks.
getGrade calculates the grade by comparing the calculated percentage with different ranges.
You can build this class in a TDD fashion by running the tests first and adding the functionality to make the failing tests pass one by one. I believe the end result would still be somewhat similar.
In the end, let’s add some assertions to test the specifications we just wrote. The final implementation would look something like this:
I discussed above that JUnit tests lack contextual information and proper grouping of related tests. You can see how every test in the spec has a hierarchy that can be used to compose complex cases.
Furthermore, for this particular style of testing, kotlintest provides an additional `And` block to allow you to create even more complex tests without losing contextual information. For example, you may want to construct a test like this:
Finally, the tests are well organized and the test results in Android Studio have that nested style too. Here’s the result of our tests in the IDE:
- Testing your code right from the start of the project provides a solid foundation for future development.
- The idea of TDD and BDD is not new, it forces you to write your domain-specific code and the architecture itself to be testable. However, due to the verbosity of JUnit and tight deadlines, testing falls behind quite often.
- Aside from code repetition, another big problem with JUnit is the lack of contextual information due to the flat hierarchy of tests. When every case is represented in the form of a class method, it becomes quite difficult to group and organize related tests.
- Modern testing frameworks like
kotlintesttakes advantage of the flexibility provided by Kotlin to allow writing more intuitive unit tests.
- The result is less boilerplate code and more meaningful tests.
Originally published at https://dev.to on September 7, 2019.