Pragmatic Unit testing in java using JUnit

Pragmatic Unit testing in java using JUnit

Why unit testing is needed? — Unit testing is when you (a programmer) write test code to verify units of code. Unit test represents small subset of the end to end behavior.

Here are the few whens and whys for writing unit tests:

  • You just finished coding a feature and want to ensure that it works as you expect.
  • You want to document a change so that you and others can understand the logic you coded into the system later.
  • You need to change code and want to make sure your forthcoming changes don’t break existing behavior.
  • You want to understand the current behavior of the system.
  • You want to know when third party code no longer behaves as you expect.

I am shouting to all Developers, it’s your job to test your code. Writing unit test code is developers responsibility. We have heard a lot of excuses from developers when it comes to unit test code.

  1. We have tight deadlines — Remember the time you spend fixing bugs. To finish work, developers write crap code and complete the feature but on production, it breaks very badly and then developers end up with fixing, debugging hours to find why this bug is causing a build to fail. If you write unit test at first place, it will reduce 80% of your bug fixing and debugging time because it will make sure your code works, and you would know where the exact code is broken and fixing could be an easy job. A good unit test covers boundary conditions which would reduce the chances of bugs.
  2. It’s not our job — We often heard from developers, writing unit testing code is not our job. First thing, there is a difference between feature testing and unit testing. Unit test code makes sure whenever you write a new piece of code it doesn’t break old functionalities. It will allow you test your previous features without running your code and makes sure your changes work properly.
  3. There is a trade off between time and quality — Unit testing doesn’t come free, however, unit test will increase your confidence and understanding of your own code. If you follow TDD (Covered test driven development in the later part) will increase the code coverage.

JUnit

Now almost all IDE’s comes with JUnit 4 preinstalled in it, or plugins available for it.

Basic of unit testing — Before JUnit 4 you had to give names to your test classes with suffix Test e.g. ExampleTest.class

and any method you write should prefix test e.g. testAddition()

Although it not required now, It is good practice to follow these naming conventions.

Create package under test → Java -> ExampleTest.class.

Your unit test methods should annotate with @Test e.g shown below

public class ExampleUnitTest {
    @Testpublic void testAdditionIsCorrect() throws Exception {
        assertEquals(4, 2 + 2);
    }
}

Assertion

Asserting your tests, Traditional methods to assert your values assertTrue(), assertFalse(), assertEquals().

Hamcrest Matchers-

assertThat() something would help against the specific result. This Hamcrest library assertion helps with some additional features.

The first argument of Hamcrest assertion is actual expression — the value we want to verify. The second argument is matcherA matcher is static method call can be imported. import static org.hamcrest,CoreMatchers.*”

assertThat(Arrays.asList(new String[] {"a", "b"}),
      equalTo(Arrays.asList(new String[] {"a", "b"})));
assertThat(account.getBalance() > 0, is(true));
assertThat(account.getName(), is(equalTo(“my name”)));
assertThat(account.getName(), not(equalTo(“my name”)));
assertThat(account.getName(), is(not(nullValue())));
assertThat(account.getName(), startsWith(“xyz”));
assertThat(account.getName(), equalTo(“xyz”));


Handling exceptions in tests

The good test always makes sure your code doesn’t break and you cover boundary conditions that could cause an application to break. Exception handling in unit test would cover unexpected input, empty/null inputs/outputs and boundary conditions. You can expect exceptions in production code throwing them from stub data.

e.g. ScoreCollection class

public static void add(Scorable scorable) {
    List<Scorable> list = new ArrayList<>();

    if (scorable == null) throw new IllegalArgumentException();
    list.add(scorable);
}

ScoreCollectionTest class

@Test (expected = IllegalArgumentException.class)
public void throwExceptionAddingNull() {
    score.addValue(null);
    ScoreCollection.add(null);
}
@Rule
public ExpectedException thrown = ExpectedException.none();
private void testAddValue(Scoreable scoreable) {
    List<Scoreable> list = new ArrayList<>();
    list.add(scoreable);
    throw new IllegalArgumentException("array should not have null");
}

Organizing Your Tests

Keeping Tests Consistent with AAA — Arrange, Act, Assert

Organize your test by applying a simple rule.

Arrange system in place. Prepare your stub data/test data.

Act — execute the test (calling method)

Assert that if code behaves as expected.

Bonus step — Using @After — release/clean up allocated resources.

Testing Behavior Versus Testing implementation

The difference between testing behavior and testing methods is subtle. When you test behavior test implementation would be abstract and even though you change the implementation it doesn’t affect the behavior of your test method, Output will be always same.

Understand the relationship between Test and Production code. You are not testing production values you’re testing your code with stub data that will behave and give the expected output.

Always write single test per method. Every test method should contain one assert value.

Write your tests as a test document which will give insights of what you want to test and what output is expected.

To keep our testing meaningful —

  • Improve local variable names
  • Introduce meaningful constants
  • Prefer Hamcrest assertions.
  • Write smaller tests
  • Move clutter to helper methods, @Before, and @After

@BeforeClass and @AfterClass — which makes sure to initiate something really important

More on @Before and @After (Common Initialization and Cleanup)

Green Is Good: Keeping Our Tests Relevant

All green all the time, if something fails, fix that test case immediately. If one of the test is failing, can use @Ignore tag. This will help tests to pass with the proper message to a developer, that test need to be covered in next phase.

e.g. ignoring failing test method

@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
@Ignore("Don't forget me!!")
public void testPrimeFactors() {
    List<Integer> imputList = new ArrayList<>();
    assertThat(factorsOf(1), is(imputList));
    
    assertThat(factorsOf(2), is(imputList));
    imputList = new ArrayList<>();
    imputList.add(3);
    assertThat(factorsOf(3), is(imputList));
    factorsOf(0);
    thrown.expect(ArrayIndexOutOfBoundsException.class);
    thrown.expectMessage("array beyond limit");
}


private List<Integer> factorsOf(int number) {
    List<Integer> primFactorList = new ArrayList<>();
    if (number > 1) {
        primFactorList.add(number);
    }

    return primFactorList;
}

FIRST properties of good test

[F]IRST: — Tests must be fast — break the test without any dependencies. follow a clean way of OO concepts. Mock objects your objects to reduce the dependencies(will cover more in the later part). 

F[I]RST: [I]solate Your Tests — Isolate your tests in small chunks. Follow Single Responsibility Principles if one test is failing, try to break.

FI[R]ST: Good Tests Should Be Repeatable — Repeatable test means it provides the same result each time you run it.

FIR[S]T: [S]elf-Validating — Self-validating, that you don’t have to run the code to check the output, you can run the tests and validate your code.

FIRS[T]: [T]imely — Unit test requires continual vigilance, so it's better to write unit tests timely and frequently to develop good practice/habit. Pair programming, peer pressure, continues deployment all would- will discuss how writing test first would help in TDD.

What to test: The Right — BICEP

Which part of the code you actually want the test. How would you decide, what to test?

When you write a test, make sure your test follows The Right-BICEP

Right, Are the results right?

B Are the boundary conditions correct?

I Can you check inverse relation?

C Can you check cross-check results using other means?

can you force error conditions to happen?

Are performance characteristics within the bound?

Boundary conditions

  • file name consists of “!*W:X&GGI)((&^%$@WQ”
  • badly formatted data fred@foobar
  • numeric overflows
  • empty missing values such null, 0, 0.0, “”.
  • Human age exceeding 150
  • duplicate values in a list
  • list ordering
  • HTTP responses that return OPTIONS response

Inverse relation

Check for inverse conditions, check with inverse input.

Cross-Checking using other means

Use different library or get it verified from another team member (Apparently we suffer from bad egos)

Forcing Error conditions

  • Running out of memory
  • Running out of disk space
  • Issues with wall clock time
  • Network availability and errors
  • System load
  • Limited color palette
  • Very high or very low-resolution videos

Performance characteristics

Base performance-optimization attempts on real data and not on speculation.

Consider using JMeter or JUnitPref for performance unit test.

Refactoring to cleaner code

Always refactor your code, break the code into small pieces. Whenever you try to refactor production code, first write behavioral unit tests and then refactor production code.

  • The implementation has changed from running code inline to running it async
  • The behavior (or result of the method) has not changed

Every refactoring turn you to unit tests. Make testable units when refactor.

Clean design is best preparation for optimisation (A detail idea about clean architecture in Android — https://github.com/android10/Android-CleanArchitecture)

Bigger Design issues

Bigger design concerns Single Responsibility Principle (SRP) — Please find about SRP here

Increase unit test coverage to boost your confidence in continual improving your design.

Using Mock Objects

Using Mockito library to mock the object.

Questions to ask before mocking objects. Does mock emulate the production work?Does production format return the formats you’re not thinking of? Does it throw exceptions? Does it return null? Do you want to return separate unit test handle these conditions? Does your test really using mocks or you are accidentally triggering production code?

Remember you are not testing production code, recognize the gap between production code and test coverage.

A mock creates hole in unit-testing coverage. Write integration tests to cover these gaps.

Refactoring Tests

These are some of the conditions where you should consider refactoring unit tests.

  • Unnecessary test code
  • Missing Abstractions
A good test is an abstraction of how client interact with the system .

Creating custom matchers would help

  • Irrelevant information
  • Bloated Construction
  • Multiple Assertions — Apply rule: Single assert per test.
  • Irrelevant details in a test- try using Before and After also helper methods to club clutter together.
  • Misleading organisation — remember AAA ?
Refactored unit tests motivates to write clean tests.

TDD

Test Driven Development is altogether different topic to discuss, I am covering some of the glimpses here.

Design your code to write tests first, then consider adding code.

It helps in ways like adding quickly a new code would be easy, fear of changing code can evaporate. You will build the system and that will give freedom to improve the code.

Starting is simple —

Follow 3 part cycle on each feature you code

  • Write a test that fails
  • Get the test to pass
  • Clean up any code added or changed in prior two steps

Testing some tough stuff

Keep it simple, Smarty

  • Try to separate the code from a thread. Write in smaller chunks and methods.
  • Trust the library code which has already implemented if you are using a library like Rx, Java 5 and above. After all, Why to waste time reinventing the wheels?

e.g — If you are using RxJava library then using in built library APIs would help to unit test code. Here we are observing the behavior of multithreaded code using TestObserver in built API of Rx to unit test multi-threaded code if you’re using Rx library for it.

Observable<EntityObject> observable = Observable.from(names).subscribe(new Action1<String>() {

        @Overridepublic void call(String s) {
            System.out.println("Hello " + s + "!");
        }

    });

observable.subscribeWith(new TestObserver<EntityObject>() {


    @Overridepublic void onError(Throwable t) {
        super.onError(t);
        assertError(t);
    }

    @Overridepublic void onComplete() {
        super.onComplete();
    }

    @Overridepublic void onSuccess(EntityObject value) {
        super.onSuccess(value);
        assertValue(value);
    }
}).assertSubscribed();

Testing on a Project

When you work with the team, it always difficult to be on the same page about unit testing. Some developers prefer test driven development, some don’t. How do we make sure that everyone follows this practice?

  • Coming up to the speed, Start with small steps. Try to reduce debugging sessions.
  • Establishing unit testing standards like Hecrest library, using Rx APIs, XCTests
  • Review via Pair programming
  • Make sure you don’t merge the code to develop branch until it is unit tested
  • Code coverage — how much code coverage is enough? is 100% code coverage practically possible? It is practically not possible to cover 100% code for unit testing, but we can start with small steps. Start with business logic, unit test your business logic then move to data layer then think of UI layer. If we try to follow TDD we could make sure we at least cover 70–80% of the code.
  • Try using in IDE’s built in code coverage tools to evaluate code coverage e.g. Android Studio- Code coverage result.
  • For Java, there are some tools which would help to evaluate code coverage. e.g Emma

To Summarize, Unit test increases your confidence on the code, you won’t be afraid anymore to change the code, reduces the bug count and fixing time. So let's Start and Continue Unit Testing!!!



To view or add a comment, sign in

More articles by Kishor S.

  • Kotlin Coroutines

    Till date there are so many ways to handle the thread in Android. Java-Thread, Thread Pools, AsyncTasks, Loaders…

  • Pro-guard set up for android apps

    First understand what pro-guard does to apk file. ProGuard is Java class shrinker, optimiser, obfuscator, and…

  • Kotlin - Extension Functions

    Kotlin supports extension functionality of the class without inherit or use of any type of design pattern like…

  • Data Classes in Kotlin

    The main purpose of creating classes is to hold the data. These classes helps to add basic functionality to obtain the…

  • Xamarin vs native applications

    Xamarin is one of the latest and proven well-performed cross-platform in the cross-platform family. I am fond of native…

Others also viewed

Explore content categories