Android Unit Testing —Choosing Naming Convention and Test Cases
Build bug-free Android apps
Maintainability is one of our goals when coding. One of the advantages of unit testing is to make the code more maintainable, please check the advantages here.
However, when we have bad code in unit tests and need to change an existing feature, this will require making changes in production code, as well as in unit tests (with their bad code).
Unit tests would be like hell or trap because we would be spending a lot of time reading and trying to understand what exactly was written, instead of helping us by making code maintainable!
In other words, if we want unit tests to make code more maintainable, we need to make unit tests as clean as possible. otherwise, a minor change will be a big issue.
Test code is just as important as production code. It is not a second-class citizen. It requires thought, design, and care. It must be kept as clean as production code.
— Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship
Choosing Test Method Name
One of the most popular guidelines that make code clean is to choose a good name for methods and variables. you can follow these tips to choose a good name for test methods for your project:
Readability
Readability is achieved by choosing a simple, expressive, and meaningful name.
One Naming Convention
It’s better to use one naming convention across your project, which makes reading test reports more understandable and consistent.
However, in some cases, you can use another convention if you are in another layer.
For example, some developers don’t like this convention:
MethodName_StateUnderTest_ExpectedBehavior
This is because it contains a method name, and their argument that unit test should test behavior not code. That means test code should not impact by changing production code. This convention tightly coupled test method to production code, which means if you change the method name in production code you should change it for any “test method” that tests this method.
As I mentioned earlier it’s tightly coupled because you test behavior not code, but what if you are testing utility class, in this case, you can use this convention because it’s more traceable since it contains a method name.
So, for business logic, you can have a naming convention that does not contain a method name, for example, this convention When_StateUnderTest_Expect_ExpectedBehavior
. and use another one that contains a method name for utility methods that require test code, not behavior.
Naming Convention Examples
There are a lot of naming conventions, following some of them:
MethodName_StateUnderTest_ExpectedBehavior
— example:getPost_success_postShouldCached
MethodName_ExpectedBehavior_StateUnderTest
— example:getPost_postShouldCached_success
Should_ExpectedBehavior_When_StateUnderTest
— example:should_throwException_when_NetworkError
When_StateUnderTest_Expect_ExpectedBehavior
— example:when_serverError_expect_postNotCached
There is no correct or wrong in choosing a naming convention as long as it’s descriptive. It depends on the developer and what he prefers.
More Readable
Some modern languages like Kotlin, allow writing test methods with spaces.
Therefore, more readable! for example, this convention When_StateUnderTest_Expect_ExpectedBehavior
would look like below
Choosing Test Cases
We cannot test all possible cases since that would be infinite! Instead, we divide input/argument of the function being tested into categories and boundaries based on our case then pick a value from each category and boundary and test them.
There is no specific number of test cases, it depends on each case. However, if code coverage is not 100% then we have to write more tests, but 100% does not mean that we covered all possible cases. it will be more clear with the next examples.
Example #1: String Duplicator
Suppose we have StringDuplicator class that has a duplicateString
method as the following
When we want to duplicate some string we can divide argument/input (in this case string) into three categories empty, one character, and multiple characters. first, let’s add one test case and run with coverage
To Run with the coverage you can choose “Run StringDuplicatorTest
with coverage” as the following
the result will be 100% since test function has covered all lines
However, we still don’t cover all test cases(three categories that I mentioned earlier) though StringDuplicator
coverage is 100%.
To cover all test cases, we can add two more test cases so, the test code will be as the following:
Example #2: Conflict Meetings Detector
Suppose we have ConflictMeetingsDetector
that has haveMeetingsConflict
method as the following:
So here we can divide arguments(in this case meetingInterval1
and meetingInterval2
) into categories as the image below
Test code will be as the following:
We’ve covered all categories, also “run with coverage” will give us 100%.
that’s great, so are we done? not yet. For this unit test, we can divide the argument into categories and boundaries. We still haven’t covered boundaries.
We easily forget about boundaries/edge cases. a lot of bugs come from boundaries. So we need to be careful when we divide into categories and boundaries.
To cover the boundaries, we can add new two test cases as the image below:
Also, two new test functions will be added as the following:
You can find full code on GitHub from here.
Final Word
Unit testing is just as important as production code. You should keep your test code as clean as possible to be able to maintain it, so unit tests will help you, otherwise, the unit tests will make your code complex and hard to maintain.
Choosing correct test cases can be done by dividing the arguments/inputs into categories and boundaries.
Also, less than 100% coverage means that we have to add more test cases, but 100% coverage does not mean that we cover every possible case.