When we are first taught programming we rarely hear about tests. We write code, compile it and run it, then “test” it by trying some different pathways in our software, to see if it works or not. This seems like the way to go, but if we apply this in production apps, users are bound to go down a path we didn’t test and find and break our software. Sometimes it may take time to find the actual issue and then some more to actually fix it, but there’s always a possibility you only fixed one of the numerous issues. This is what testing aims to solve.
There are multiple ways to test software, all the way from the unit and integration tests to end-to-end testing. Unit testing refers to testing small, atomic parts of the code that represent just a tiny piece of the whole, like a formatting function. Integration testing means we test multiple units performing together, for example, testing if a given pair of functions working together produces the expected result. End-to-end tests, or E2E for short, test the functionality of the big picture, like login in an app.
So how can we write unit tests? A basic unit test looks something like this:
We create a class containing such test cases in separate sources directory, usually colored green in an android studio and called “tests”. Functions annotated with @Test will be run and if they return without throwing an exception the test will pass. The annotation comes from the JUnit framework, the de-facto Java testing library. Note, however, that every function is called on a fresh instance of the class. So what if we need to set up something before tests? What if we need to do some cleanup after each test? What if something needs to be set up after all tests and cleaned up after all of them are done? That’s how:
The annotations should be self-explanatory, note that the @BeforeClass and @AfterClass annotations need to be placed on static methods. So now that we got the basics down we can start the next important part: the assertion. We don’t just test functions that sometimes throw an exception and always expect them to pass, sometimes we pass parameters in them and expect them to return something based on that parameter. We can easily assert that the correct value is being returned via the static functions in org.junit.Assert class. Some examples include asserting for null values (assertNull, assertNotNull), (non) equality (assertEquals, assertNotEqual) and others. These functions do pretty much what their names say, they require their parameters do fulfill the condition, otherwise, they throw an assertion error, which fails the test. But what if we want to test if the function throws an exception? Well, @Test annotation has an optional parameter called “expected”. We can set it to a class of the exception that we’re expecting to be thrown in the function, and the test will only pass if an instance of the specified exception is thrown.
Once you start writing more tests, you might notice that it’s best to write more, shorter methods, because it’s easier to test them. Sometimes however you depend on existing classes and depend on changing their behavior to test one of your functions. It’s best to “mock” the method. We can easily do that using Mockito. Using mockito we can change what a method does, what it returns, even assert that a method was called. Let’s say we want to mock an interface, to always return the same string every time it’s called. Just to prove a point, we’re also going to verify that it was actually called:
Note the “when” method call. This call starts the mocking and allows us to return the value we want. The call to “verify” tests if the method was called. We can also add a parameter that specifies how many times it was called. By default, the methods do nothing and return the default value (null for objects, 0 for numeric and false for boolean methods). If we want to do the same on an abstract class and keep an implementation of a function we can use the function Mockito.spy, which allows us to mock methods, verify them being called and still keep their original implementations.
Another thing that the unit tests allow us to do is helping us learn the library. Let’s say we want to start using a new library, but don’t have experience with it yet. With unit tests we can write some functions that test the functionality we expect, then use what we learned in our actual project. This also serves as a sanity check, for example, if a library changes it will no longer pass all of our tests, which in turn will inform us that our main code needs to be updated as well, saving us quite a bit of work.