We all have been there: we spent so much time into a project, we make sure to run each and every possible scenario we might think off in order to make it as good as possible. But when you let someone else try it out they find small edge cases that can make your app behave in unexpected ways.
In order to prevent that from happening too often, we use different types of testing techniques: Unit Testing, Integration Testing and End-To-End Testing. Although the later (End-To-End Testing) can be made by either developers or Quality Assurance team members.
This is also known as the Testing Pyramid
Try not to be distracted about the other two types of test mentioned before, but rather focus on the idea that we can expect our unit test to be far more in quantity than the others, but keep in mind that they don't replace each other. They are all important.
Enough chitchat, let's start coding, shall we?
Requirements
- Have the JDK installed on your machine (duh!).
- An IDE (I would recommend IntelliJ IDEA Community Edition).
- In terms of Java as a language, you need to be familiar with the concepts of a
variables
,constant
,function
,class
andobject
in order to fully understand this post. (Which I might be able to help with this). - Add the Math
class
from this GitHub Gist to your project.
Getting Started
The very first thing we need to do is to add the TestNG framework to our project. This will provide us with a set of classes
and annotations
which will come in handy later.
Add TestNG to your project
This can be done 2 ways: manually or using Maven in your project. Feel free to skip the other depending on how you setup your project.
Manually
You can follow this guide in order to add it manually.
Via Maven
- Open up your
pom.xml
, which should be on your project's root folder. - Inside the
<project>
tag, make a<dependencies>
tag. - Add the following block of XML code:
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.14.2</version>
</dependency>
In case you haven't done it, make sure to select the
Enable auto-import
option that should appear on the bottom right of your screen. This will allow IntelliJ IDEA to automatically detect any changes made to yourpom.xml
and refresh it accordingly. Life saver :)
The test subject
For the sake of this post, we will not be applying TDD (Test Driven-Development) techniques, our focus will be on getting to know how to write test classes and methods.
So, in order for us to start, we will need to create a class
inside our src/test/java/
directory, and name it MathTests
.
The use of the suffix "Tests" at the end of a testing
class
name is a common naming convention that will allow other developers and yourself to quickly know, without opening the file, that it has testing logic inside of it.
Which should look like this:
public class MathTests {
// ...
}
Can you spot any difference from a regular class
and this one?
Nope.
And that's fine.
What makes a class
a "testing class" is not it's signature, but actually it's methods and it's important for these classes to be inside a directory marked as a Test Source Root.
These methods should follow a particular naming convention, should have the public
access modifier, should have void
as their return type and the @Test
annotation before their signature.
The
@Test
annotation indicates our IDE's test runner that this is not a regular method and will execute it accordingly.
So, our first test method would look like this:
@Test
public void add_TwoPlusTwo_ReturnsFour() {
// ...
}
Adding testing logic
There's another convention that is really common among other programming langauges, known as the Tripple A:
- Arrange: consists of a few lines of code that are used to declare and initialize the objects we need in our test.
- Act: is usually a few lines of code where we perform the actions, whether it is some calculation or modify the state of our objects.
-
Assert: usually consists of a single line of code where we verify that the outcome of the Act part were made successfully. Comparing the
actual
value with anexpected
value that we plan to get.
In practice, this would be:
@Test
public void add_TwoPlusTwo_ReturnsFour() {
// Arrange
final int expected = 4;
// Act
final int actual = Math.add(2, 2);
// Assert
Assert.assertEquals(actual, expected);
}
The
final
keyword is used to prevent our value to be changed later, also known as a constant.
Here we can see how the whole arrange, act and assert come together.
If you take a look at the line numbers on the left of the add_TwoPlusTwo_ReturnsFour()
method, there's a green play button, select it and then choose the first option from the context menu.
Wait a few moments and... the test runner panel should open up with the test results.
If you see that everything is green, it means that our test passed!
But, as a good tester, we should also try to make our test fail.
Let's change the act part so it adds 3
and 2
together, so our actual
value becomes 5
and our test should fail.
...
Did it fail?
Great!
Now, some of you may be wondering why we use the assertEquals()
method from the Assert class
, we could manually try to use an if-else
block that can simulate the same results, but the Assert class
provides a handy set of methods to do various types of validations.
The most common ones are:
-
assertTrue()
: evaluates a givencondition
orboolean
, if it's value it'strue
, the test will be marked as PASSED, otherwise, it will be marked as FAILED. -
assertFalse()
: similar to theassertTrue()
, but whenever thecondition
orboolean
isfalse
the test will be marked as PASSED, otherwise, it will be marked as FAILED. -
asssertEquals()
: commonly used to compare two given values that can be either primitive types (int, double, etc) or any objects.
If we were to implement our own logic using if-else
, not only it would clutter our code, but also could lead to unwanted results. Since, if we forget to throw
and exception in one of our if-else
blocks, both of our code paths will be marked as PASSED.
Tip: most of the time we should only use 1 single Assert method per test, although there are exceptions to this rule. But this is normally recommended in order for our test to be really small and straight to the point. Each test should only verify 1 code path at a time. Also, if we had 3 assertions and the first one fails, the following ones will never be executed, so keep that in mind.
Now that we got that out of the way, let's continue with more tests!
How about you practice testing more scenarios for the add()
method?
- Add a positive number with a negative one.
- Add two negative numbers.
If we take another look at our Math class
, we can see there are 2 more methods.
I'll let you do the tests for the multiply()
(hint: make sure to test when we multiply a number by zero) method and I'll focus on the divide()
one for the rest of this article.
The divide()
method
Let's take a closer look to this method:
public static double divide(int dividend, int divisor) {
if (divisor == 0)
throw new IllegalArgumentException("Cannot divide by zero (0).");
return dividend / divisor;
}
As you can see, if the value of the divisor
argument is 0
, we will throw an IllegalArgumentException
, otherwise, the division operation will be performed.
Note: the
throw
keyword not only throws a given exception, but also stops the code execution, so it works similar to thebreak
keyword inside a loop or aswitch
block.
So, this method has 2 possible outcomes or "code paths". We need to make sure to test them.
The amount of tests per method, should be equal or more than the amount of code paths it has.
Which means, that we should at least have 2 tests.
Let's go ahead and make them!
- Divide two numbers, where the
divisor
is any number but zero (0). - Divide two numbers, where the
divisor
is zero (0).
Our first test would be something like:
@Test
public void divide_TenDividedByFive_ReturnsTwo() {
final double expected = 2.0;
final double actual = Math.divide(10, 5);
Assert.assertEquals(actual, expected);
}
And our second test would be:
@Test(expectedExceptions = IllegalArgumentException.class)
public void divide_TenDividedByZero_ThrowsIllegalArgumentException() {
Math.divide(10, 0);
}
Wait wut!
Mr./Mrs Reader: "B-bu-but what happened with the arrange, act and the assert? what is the expectedExceptions
part doing?"
Do not worry, I shall explain shortly!
- I decided to skip the whole arrange, act and assert because the execution of our code will automatically be interrupted when the
divide()
method is ran. So the whole Tripple A can be omitted for this test in particular. - The
expectedException
part is needed in order to tell our test runner that theIllegalArgumentException
is actually possible to happen in this test, if we were to change that to another exception, our test would fail.
Tip: remember to use the
.class
at the end of the exception name, otherwise, this code would not compile.
Testing objects
You have noticed that so far we have been testing static methods of our Math class
, which means we don't have to create objects of it. Which is fine.
But what if we had a class
that didn't have static methods?
For this, our testing framework (TestNG) provides a pair of annotations to make sure that each of our test use a fresh instance of our class
.
Let's imagine we could create instances of the Math class
.
In that case, our tests would look like this:
@Test
public void add_TwoPlusTwo_ReturnsFour() {
final Math math = new Math();
final int expected = 4;
final int actual = Math.add(2, 2);
Assert.assertEquals(actual, expected);
}
@Test
public void divide_TenDividedByFive_ReturnsTwo() {
final Math math = new Math();
final double expected = 2.0;
final double actual = Math.divide(10, 5);
Assert.assertEquals(actual, expected);
}
Which isn't that bad, but remember that we can make many more tests for this same class
and having this Math objects initialized over and over will create more code noise.
If we have to ignore certain parts of our test, specially in the arrangement, it means we can use one of our testing framework's tools:
@BeforeMethod & @AfterMethod
These two annotations can be added to our test functions like we have been using the @Test
one, but they work in a particular way.
-
@BeforeMethod
: this code block will always be executed before any other@Test
method. -
@AfterMethod
: this code block will always be executed after any other@Test
method.
So, why would we use them?
In all of our @Test
methods we would have to constantly initiate a new Math object, so with the help of the @BeforeMethod
annotation we can get rid of this repetitive code.
First thing we need to do is to promote our Math object to a member variable or property.
public final class MathTests {
private Math math;
@Test
public void add_TwoPlusTwo_ReturnsFour() {
final int expected = 4;
final int actual = math.add(2, 2);
Assert.assertEquals(actual, expected);
}
@Test
public void divide_TenDividedByFive_ReturnsTwo() {
final double expected = 2.0;
final double actual = math.divide(10, 5);
Assert.assertEquals(actual, expected);
}
}
Then add our @BeforeMethod
function, which is commonly named as "setUp".
public final class MathTests {
private Math math;
@BeforeMethod
public void setUp() {
math = new Math();
}
@Test
public void add_TwoPlusTwo_ReturnsFour() {
final int expected = 4;
final int actual = math.add(2, 2);
Assert.assertEquals(actual, expected);
}
@Test
public void divide_TenDividedByFive_ReturnsTwo() {
final double expected = 2.0;
final double actual = math.divide(10, 5);
Assert.assertEquals(actual, expected);
}
}
Now, in order to make sure we clear out our math
object, we can set it's value to null
inside our @AfterMethod
function, which is usually called tearDown()
:
public final class MathTests {
private Math math;
@BeforeMethod
public void setUp() {
math = new Math();
}
@Test
public void add_TwoPlusTwo_ReturnsFour() {
final int expected = 4;
final int actual = math.add(2, 2);
Assert.assertEquals(actual, expected);
}
@Test
public void divide_TenDividedByFive_ReturnsTwo() {
final double expected = 2.0;
final double actual = math.divide(10, 5);
Assert.assertEquals(actual, expected);
}
@AfterMethod
public void tearDown() {
math = null;
}
}
This means that the order of execution of our test would be:
- The
setup()
. - And
add_TwoPlusTwo_ReturnsFour()
. - Then
tearDown()
. -
setup()
again. - And
divide_TenDividedByFive_ReturnsTwo()
. - Then
tearDown()
again.
Aaaaand that's it
With this you should be more familiar now with how Unit Testing works.
Although we didn't do any tests that required us using the assertTrue()
and assertFalse()
, I encourage you to do your own tests to play around with them for a little bit :)
Feel free to leave a comment if you have any questions and I'll do my best to clear them out!
If you would like to take a look at the entire project, head over to this repository on GitHub.
Top comments (17)
Great intro. I hit an error on line 11 of the final code. I get an
Error:(11, 16) java: Math() has private access in Math
. IntelliJ's linter is yelling about it as well. My Java knowledge is minimal so I'm wondering how would I fix this error? I'm guessing it has something to do with line 2 ofMath.java
.Thanks for letting me know, Seth!
That's my fault.
Try removing this from the Math.java file:
The entire class should be like this now:
In case you or someone else also wonders why, the
private Math() {}
refers to the constructor of our Math class, I made itprivate
at the beginning because all it's methods arestatic
, which prevents anyone from trying to instantiate it. But later on I decided to also add an example where we had the need to use an object and I forgot to update it hahaha.That works. Thanks!
Hi, just a small hint. In case you add a dependency in Maven which is only intended for testing, which TestNG is, you should do it like this:
Apart from that if suggest to name a test class
*Tests.java
you have add an example to use the most recent versions of maven-surefire-plugin (2.21.0+). Otherwise this will not being executed as test. The best it to name it*Test.java
this will work with older versions well..Thank you, Karl!
That's really helpful π
Just to give some other options: Weβve just started using JUnit 5, the best thing is actually @DisplayName to price a readable test name. Also, we switched to AssertJ that has a pretty neat fluent API.
Very well explained as usual (> ._.)> Kuddos!. This is really helpful since Im trying to implement a new testing framework for the folks at work, wish you luck and here your reward.
Hahahha, thanks Manuel!
I'm glad you found it useful π€
Great post
Great article!
Hey Christian,
Which IDE do you prefer? :)
Good article!
Hi Francesca, I would say that IntelliJ IDEA is my favorite overall. But I've also found text editors with plugins to be really good as well with a couple of plugins like VS Code.
Awesome guide. This is a great refresher for me as I have not wrote some unit tests in a while π¬
One of the best articles that I found on the whole web, Thank you, sir.
But I got "Error:(3, 34) java: package org.graalvm.compiler.debug does not exist" when I type expectedExceptions.
Hey Mohammad,
Thank you very much!
I'm not entirely sure what might cause it, but it seems you are missing a dependency.
In case it might help you, here's a repository with the project I used while making this article: github.com/chrisvasqm/intro-unit-t...
This is a great post. thank you very much Christian Vasquez