1. Write your own tests!
Throughout the exercises of PRC1, you have become acquainted with the value of tests: You have a way of checking if your code is any good without having to test each and every part manually. You clicked on the nice TMC button in NetBeans and then you could see which parts of your code worked, and which didn’t. There is a catch though: Out there in the real world, there won’t be any NetBeans button doing that magic for you, nor will some teacher or school provide the tests for you, so you will be on your own.
But fret not! Writing tests is typically much simpler than writing the actual code. At least, it is when you follow a basic set of steps:
-
Arrange: Prepare or set up the thing you want to test
-
Act: Interact with the object you are testing
-
Assert or Ensure that the observable result(s) is/are as expected.
-
If it says boom, then at least you learned something ….
Topics week 1
-
Test Driven Development.
-
Maven configuration as exercise.
-
Arrange Act Assert
-
JUnit (5) as test framework.
-
AssertJ as assertion library.
2. Testing / Test Driven Development
2.1. What are tests and why do we need them?
The way that you have worked with Java so far is that you had to write some code
to implement something. For example you had to implement a bird watcher system
or a telephone book, in which you could save people with their telephone numbers
and look them up, too. You probably created classes called PhoneBook or
BookEntry,
and you had variables such as String personName
, int phoneNumber
and maybe a
List
or a Map
which contained all the people and their phone numbers. You had to
think about how the phonebook works, and then you added classes, fields (variables)
and methods. Slowly but surely, your phonebook started to look more and more
like a phonebook.
But how did you know that your phonebook was actually working? You could have implemented a method that didn’t work, or maybe you forgot to add people to the phonebook, so the data wasn’t actually stored. What you did was you ran the TMC tests. They kept telling you whether the phonebook was working as intended. The test said "when I look for Alice in your phonebook, I will receive Alice’s phone number 1234-567.". And if the test didn’t receive Alice’s number, it would turn red and say something like "I expected 1234-567. But I received 0042-987." Therefore, the test fails.
The tests were a really useful way for you to know that you were indeed implementing a proper phonebook. You could have just written empty classes and said "here is the phonebook!" and there would have been no way to verify that the phonebook works. The test made sure that this doesn’t happen. They also give you confidence as you code. Because every test that turns green gives you that little "yes!" feeling that you wrote something that works.
Thus, tests fulfill two functions at once: they constantly probe your implementation and check whether it works, and they make you feel more confident about your programming skills. That’s really neat!
When you were at school, you probably had to write an English exam at some point. You wrote your answers, and then the teacher graded your exam. The teacher is just like a test: she reads your exam, expecting the correct answer. For example, you wrote "Alice and Bob goes to school.". Your teacher would get the red pen and highlight the word "goes". The teacher says "I expected: Alice and Bob go to school. Instead, I received: Alice and Bob goes to school." The teachers expectation, or assertion, was not met. Therefore, you get an error. As you grow older and you become more proficient at English, you write Emails in English, perhaps for work or at university. When you make a mistake, you spot your own errors: "oh hang on, I have to use 'go' instead of 'goes' here." You have internalized a test that checks for the correct conjugation of the word. You know what to expect, and when you deviate from the expectation, you spot the error.
Many people work the same way when they write code. They say "I know what I am doing because I have experience and I know the rules." But of course, we always make mistakes. Our brains are really bad testers. We stop seeing mistakes because we feel so familiar with our code. Have you ever handed in a report for university, only to find that the lecturer finds spelling errors you swear you didn’t do? That’s what happens in programming, too.
That is why we write our own little annoying English teachers that constantly check: is this correct? Even though we know how to program, we also acknowledge that our brains are terrible testers for things that you write yourself, so we give our brains a break and write a test instead.
2.2. Test Driven Development (TDD)
In Week 9 we wrote a little phonebook and then we ran the test to check that the phonebook was working. That worked well, because we no longer have to rely on our brains to spot the errors on-the-fly. But here is another bad message: not only does our brain stop recognizing errors in our own code, but it is also automatically biased to want our own tests to pass. You spent all this time writing your phonebook, you know how it works, so you know what test to write for it.
But your brain secretly deceives you: you have implemented a phonebook, and this is the only phonebook that you know. What if you removed your code, but leave in the tests- and ask a friend to write a phonebook? They start complaining that your tests are unfair. "Why do I have to use a List? I use Maps!" your friend says. But your tests insists that the phonebook has a List. So what have you done? You have written a test that proves YOUR implementation is correct.
When you work test-driven, you don’t implement the phonebook and then test it. You first write the test for a phonebook, and then you implement it. That sounds a bit weird at first. Why write tests for stuff that’s not there yet? We know it’s going to fail! But this is what we want. We need to keep telling our brains: this does not work. Figure out a way to make it work. This way, it is much harder to get married to your own code and to stop seeing problems with your test. You have found a way to deal with the human brain. Congratulations!
2.2.1. So how do I know what to test?
There is a little (actually, it’s big) catch with TDD. When you write a test for a thing that doesn’t exist yet, how do you know what to test for? If I write a test for a phonebook that is not implemented yet, what does my test expect?
The truth is, you never get around having to make implementation decisions. We just try to minimize our margin for error. So when you start writing your first test for your (non-existing) phonebook, you HAVE to have an idea of what the phonebook is supposed to do. So perhaps you start with "I expect that the phonebook lets me look up a person. When I look up Alice, I expect Alice’s phone number.". What could such a test look like?
2.2.2. Test selection strategies
There is no golden rule that says "always start with text X, and then test Y, and finish with test Z.". Instead, we rely on heuristics: rules of thumb that guide us through the TDD process. There are several decisions that you can make when writing tests.
For instance, you can decide to first write tests for all elements of the system under test (SUT), but not go into detail yet what each SUT needs to have tested. This would mean focusing on width first, and depth later. For instance, you could write tests for the entirety of the phonebook, but you don’t, for instance, test each valid or invalid phonebook entry. The other approach would be to start with the details on one particular part of the test class, and moving on to other parts only when one section of the SUT is covered completely. In this version, you go depth-first instead of width-first. Both approaches have their merit and you need to decide which strategy is best in which situation.
Another strategy is to weigh the difficulty of each test. When you look at a list of tests to write, do you start with the easy ones that are implemented quickly, or do you start with the more difficult tests that require more implementation thinking? When you are stuck on a particular test, it might be useful to first implement a number of tests that are easier to write, so that you make progress. It might be that whatever was causing you to be stuck on that test is now easier to solve now that you have a list of other tests that you have implemented.
2.2.3. Where to start
Choosing is always hard, including choosing where to start. The typical order for test driven development is:
-
A Constructor. You typically need instances and this is how you can construct some.
-
Getters for the fields that are defined in the constructor. If such getters are not part of the public API of the class, make them package private, to hide them from client classes outside of the package.
-
The
public String toString()
method, which can show the result of the constructor. Note that toString is often used for debugging, and that is what the auto-generated to string typically is for. -
Setters, if needed. (They are NOT in the fraction exercise). If you can get by with a class without setters, you are typically better off.
-
The business methods, one by one. In case of Fraction exercise, start at multiplication, because that is easiest.
-
Further refinements, like in case of the Fraction exercise, that the fraction should always be in normalised form.
Never add a member (method, field) to a class unless you have a test that proves the need of it. |
Exercise First Contact
Prepare IDE and Maven
Prepare Your Workbench
This exercise does not have to be committed. In only helps to prepare and configuring your IDE and in particular maven.
Create the project in NetBeans as a Maven Java Application.
Adapt the pom file of the project such that it uses testeasypom as parent. You do that by declaring
the parent directly after the second line that reads <modelVersion>4.0.0</modelVersion>
. This will have
the effect that you will have the exact plugins for the style of testing we want to teach.
<parent>
<groupId>io.github.sebivenlo</groupId> (1)
<artifactId>testeasypom</artifactId>
<version>4.0.5</version>
<relativePath/>
</parent>
1 | Testeasypom lives at github in a public repository and is also published in the maven central repository. |
@Test
void firstContact(){
Greeting g = new Greeting( "Johnny" ); (1)
String greet = g.greet();
assertThat(greet)
.as("expecting polity greeting")
.contains("Hello", "Johnny");
}
1 | You will have to type this without completion, because nothing the Greeting class exists at this moment. |
You will notice that when you type (or copy) the code above in the IDE, that compiler and IDE will not like your code, because the class Greeting does not exist and on top of that has not method called greet()
that returns a String. Do not worry because the IDE is willing to help. Look for tip
and click on it. From the suggestions select create class in Source directory to create the Greeting class in the package
hi
.
Then for the greet method, select the create method in the Greeting class.
Up until now you made the compiler and IDE happy.
Run the test. It should fail. Red is the color we want here.
Then make the greeting class pass this test. You have to make the greet method pass it’s test. Make it green.
Bravo: you have just created your first method in Test Driven Development style.
If the IDE (or the compiler) complains that it can’t find things, or something along that line, you should do what is called a priming build: Just build once by pressing the hammer symbol or right-click on the project and select build. This will trigger maven to fetch all dependencies, that is the libraries that are used by the project. Testeasypom selects test specific libraries to make sure that we are all on the same page regarding the test dependencies in the project. |
3. Arrange Act Assert
When designing tests it is good to keep the ideas of making action movies in the back of your head.
Looking at some the making of… can be quite instructive in this case.
For a scene to be taken, there is typically quite some preparation involved like hanging Spiderman
on a thin cable from the ceiling, making sure the crook is in the right position so he can land or receive the first punches and so on.
Then the director calls action and the scene is acted out
If the director is satisfied, al is okay
If not, he will want a redo later on.
A very popular style to write unit tests is the triple-A or Arrange-Act-Assert.
Arrange simply means that you set up the objects you need in the tests:
-
The System Under Test, the SUT its the term in the tesing world. In movies it is the protagonist or main actor.
-
The Dependent On Components or DOCs, the scenario needs to play out the action. Supporting actors or even props in the movies.
Act is quite similar to a film directory shouting action, that is do what you prepared for.
-
This typically is one method call on the SUT.
Assert to test if the actual result of the action is what is expected.
-
The actual value can be the return value of the called method, but also the (new) state of the SUT. In the movie: did the explosion happen, was the punch landed correctly and was the emotion credible.
@Test
public void shouldAllowToAddAddress() {
// Arrange
Client client = new Client();
Address addressA = new Address(221b, "Baker Street");
// Act
client.addAddress(addressA);
// Assert
assertThat( client.getAddresses() ).contains( addressA ));
}
Of course, Arrange, Act and Assert will vary with the actual business requirement, much as the setup, play, and act in movie making. And also, quite similar to movie making the arrange can be expensive or cheap, the action be fraught with risk or easy, and the assert can be complex or simple.
We hope to teach to keep all three as simple as possible, to keep the costs of testing so low that to make it never an argument to skip testing.
Later in this course we see some more complex scenarios, in which we need Extras as one would say in the movie industry.
4. Clues needed
A smart person is able to avoid work. A common strategy is to let others do it for you. You can achieve such smartness as well by using your tools properly.
For instance your IDE will gladly complete code that your started by showing a list of possible completions of the code that you already provided. However, the IDE can only do that if you provide it with sufficient clues. For instance when you want the IDE to generate a non-existing generic class, create two instance with different type parameters before your accept the hint to create the class.
Box<String> sb= new Box<String>(); // compiler will complain for non existing Box class.
Box<Integer> ib= new Box<Integer>(); // compiler will complain for non existing Box class.
// now you can let the IDE to follow its urge to do the typing for you.
The same applies to methods. If you want a specific return type, provide that in a declaration where you specify such type.
It also applies to frameworks such as AssertJ, that provides an assertThat(…)
method as entry for almost all asserts.
But what you put on the dots is important, because the IDE will inspect the type of the expression and then will look for the possible
completions. The completions for an Integer or int are very different from those for a Collection
or Iterable
.
Combine this with the previous tip. First specify a variable with a type, that
use the variable inside the assertThat(…)
and the IDE will be able to find the available completions.
Sometimes you have to nudge the IDE a bit, for instance when it suggest to create two constructors from the above example. Accept one of the suggestions, but then modify the generated constructor to accept the type parameter of the class as input type of the constructor. After saving all files, you will have convinced both IDE and compiler.
it is good to have a thinking pause
|
Exercise PhoneBook.
Walking-through exercise: Phonebook
In this exercise, we will write our own phone book. As this is the first exercise, we will work through the solution and have some accompanying text for you to read. Please read the text and don’t just copy+paste the code, especially when you are struggling with something.
Let’s write our own tests and work test-driven to implement our phonebook again.
Turning requirements into testable statements
Let’s get started with writing our phonebook, TDD style. How do we proceed? In the beginning
it’s best to write down the requirements for the system as a list of requirements that is testable.
Instead of writing "The system should add numbers together", write "2 and 2 should equal 4.".
The first statement is not testable, the second statement is much easier: assertThat(2+2).isEqualTo(4)
.
The list of requirements reads:
-
adding a phone number to the relative person
-
phone number search by name
-
name search by phone number
-
adding an address to the relative person personal information search (search for a person’s address and phone number by name)
-
removing a person’s information
Let’s turn each requirement into testable statements and how we are going to verify them.
-
Lookup a person by name:
-
Input "Jukka" should yield Null (because we haven’t added any entries yet)
-
-
Add person and phone number:
-
input "Pekka", "040-123456" should store those values
-
lookup: "Pekka" should yield "040-123456"
-
-
Lookup a person by number:
-
input "02-444123" should yield Null
-
input "040-123456" should yield "Pekka"
-
-
Add address:
-
input "Pekka" and "Hulsterweg 6, Venlo"
-
-
Lookup personal information by name:
-
lookup "Pekka" should yield a String containing address "Hulsterweg 6, Venlo" and phone number "040-123456"
-
-
Deleting personal information by name:
-
Input "Pekka", should delete "Pekka" and number and address
-
The requirements leave some room for several designs.
Before beginning with the list, this is what our test class looks like.
To avoid creating a new phonebook object in every test, we use declare a field in the test class and initialize it with a new PhoneBook()
.
public class PhonebookTest {
// create the
Phonebook phonebook= new Phonebook();
//
}
Note that JUnit creates a new test class instance before every test method is run. So for each test, everything is fresh.
Let’s work our way through that list. In the beginning, we want to have a phonebook that actually looks up people.
Because we haven’t added anything to the book yet, we’re starting with the test case that the person is not found.
We could have written the test differently; instead of testing isNull
, we could have asked for isEqualTo("person not found")
.
The reason we are not doing that is that we are making the test brittle (prone to break): by expecting a very specific String output,
we essentially force our phonebook implementation to output "person not found".
This is not the point of a test. You want to make sure that you test that functionality "if person is not found, the method should not return a person".
It’s intuitive.[1] to use the isNull
comparison here.
@Test
public void phonebookSearchByNameNotFound() {
assertThat( phonebook.searchByName( "Jukka" ) )
.as("Person not present should return null")
.isNull();
}
Next, we add a person and use the searchByName
function to see whether the adding was successful.
To check whether we have successfully added an entry to the phonebook, we make use of the (already tested) search function.
Because the search function was already successfully tested, we can include it here.
Can you figure out what weakness our test has? We are using the contains
comparison to see whether our phonebook contains the data we ask for.
The phonebook could essentially be a long String containing all the data you add, but it has no phonebook structure that,
for example, a Map
or a Linked List
would provide. At this point we have to decide how strict we make the tests.
We could add another assert
statement that checks the length of the String. If it’s greater than what is needed two contain name and number,
the test fails. This would be a more strict test, but it also enforces a certain type of implementation.
Essentially, when writing our tests we constantly make judgment calls of how lax or strict our tests should be. Let’s leave our test like this for now.
@Test
public void phonebookAddsEntry() {
phonebook.add("Pekka","040-123456");
assertThat(phonebook.searchByName("Pekka"))
.as("An added person, phone number should be found")
.contains("040-123456");
}
We have successfully tested the searchByName
method and the add
method.
Moving down our list of requirements, next is the searchByNumber
functionality.
Let’s write a test that will first add an entry to the phonebook, and then asks to lookup a person by providing their number.
In addition, we write another test that checks that looking up a number which is not in the phonebook returns a null result.
@Test
public void phonebookSearchByNumber() {
phonebook.add("Pekka", "040-123456");
assertThat(phonebook.searchByNumber("040-123456"))
.as("search by number should return person's name")
.contains("Pekka");
}
@Test
public void phonebookSearchByNumberNotFound() {
assertThat(phonebook.searchByNumber("02-444123"))
.as("search for number not present should return null")
.isNull();
}
Are you getting the hang of it? Remember that our tests don’t prescribe one implementation- each
of you can write a different phonebook implementation that all pass these tests. The more tests we write however,
the more we have to optimize our implementation. We will see that with our next set of tests, the address entry.
According to our requirements, the phonebook should also contain the address of the persons added, and naturally,
it should be possible to look up addresses when specifying a person’s name. Again, our test forces us to
make an implementation decision. In the code below, addAddress
is a separate
method and the searchByName
method is now expected to yield the address as well.
@Test
public void phonebookSearchAddress(){
phonebook.addEntry( "Pekka", "040-123456" );
phonebook.addAddress( "Pekka", "Hulsterweg 6, Venlo" );
assertThat(phonebook.searchByName( "Pekka" ))
.as("after add, parts of the address required")
.contains( "Hulsterweg 6", "Venlo" );
}
Using an API properly can boost your application’s performance. For instance when you first ask a map if a key is contained and then ask it to retrieve it is effectively asking the map to do the same information twice, effectively doubling the work. In such case, simply ask for the bloody thing. The map will return null if it does not have it, and checking for null on your side is just as efficient as checking for the boolean return value of contains. If you forgot, revisit the part on how maps are created in PRC1. Or look at the sources of HashMap. |
Here is what an alternative test could look like:
@Test
public void phonebookSearchAddress(){
phonebook.addEntry( "Pekka", "040-123456", "Hulsterweg 6, Venlo");
assertThat( phonebook.searchAddress( "Pekka" ) )
.as("addEntry should add phone and address")
.contains( "Hulsterweg 6" ,"Venlo", "040-123456" );
}
In this version, the addEntry
method is required to take a third argument, the address;
and there is a specific search function that looks up addresses only. Which of these two versions
(or another version entirely) you choose is down to the business logic and best coding practices.
If the business logic requires a separate address search for example, the second test is the way to go.
But the way we have phrased our requirement, the first version is correct.
Another tip on maps: If you iterate through a map, only to retrieve key and value when the key is found, do not iterate through the keySet to find the key, but instead use the entrySet , because that also
avoids double work on the maps side. The entryset is the set of key-value pairs, so you will have both in one go.
|
New tests can have an impact on our implementation. Perhaps up until now you have used
a HashMap<String, ArrayList<String>>
to store a persons name and their phone numbers.
Now that people also need addresses, our simple HashMap
implementation reaches its limits.
Sure, we could store numbers and addresses in the same ArrayList
, but that would be messy.
Instead, we could use Object-oriented principles and create a Person
class that holds name,
numbers and addresses and the HashMap<String,Person>
binds people’s name and person Objects.
Another one on performance. If you iterate through a collection or BookEntries, do not create intermediate objects, like with concatenated Strings, only to reject them when they do not suit the search. Instead inspect the list for containment or use a smarter data structure such as a Set , which can check for containment very efficiently. If you want to keep the entries in the set in insertion order, so it behaves similar to a list, choose the LinkedHashSet to do your bidding. If you do not expose your internal data structures you can change them without changing the functional behavior of your class, but at a better performance point. |
How you implement your phonebook is up to you, but we can see here that as testing progresses,
we are forced to refactor our code and to optimize it so that it passes the tests,
yet its easy to read and implements good design choices. The key point here is that our test
should not worry about whether you implement a Person
class, but is concerned
solely with the outcome. That way, different implementations can achieve the same end result.
Lets return to our list of requirements. We have one requirement left, the deletion of an entry. In our requirement we specified that we lookup a person by name before deleting. Let our test reflect that:
@Test
public void phonebookDeletesEntry() {
phonebook.addEntry("Pekka", "040-123456");
phonebook.deleteEntry("Pekka");
assertThat(phonebook.searchByName("Pekka")).isNull();
}
Again, we are using the searchByName
method to verify that deleting the entry actually
removes the data selected.
And this is it! Our very first test-driven phonebook!
5. AssertJ examples.
In our course we to stick to the AssertJ library where possible.
The rationale for that is:
-
The AssertJ assertion API is very powerful and can easily turn overly strict or brittle tests into more effective tests.
You have come across tests in the mooc that insisted on specific formatting. With AssertJ you can specify the elements you want to see and those you do not want to see. See examples below. -
AssertJ tests tend to be quite readable, once you get used to the fluent style. You will see long method names, but they help understanding what is going on a no-brainer.
-
AssertJ can protect the test methods against unexpected exceptions, such as null pointers (NPE) before doing further tests.
-
With the exception of testing exceptions, the user interface and API of AssertJ is quite consistent.
You always start withassertThat( objectOfInterest ).assertingMethod(…)
.
We try to use the latest stable version of AssertJ and Junit 5.
Either use testeasypom version 3.0.0 and up or add assertj.core to your test dependencies. |
5.1. Simple Tests
car.start();
assertThat( car.motorIsOn() ) (1)
.as("Motor should be on after start") (2)
.isTrue(); (3)
1 | Object of interest, return value of method, which in the test fails if not true. |
2 | Optional description, use in particular when testing primitive values such as boolean. Use it to improved the information you get when the test fails. |
3 | The actual verification. Use isTrue() and isFalse() for boolean , isEqualTo() and isEquals() for other types. |
assertThat(car.speedLimit())
.as("Make it a legal car?")
.isEqualTo(250);
assertThat( f1Champ ).isEqualTo( louis );
assertThat( f1Champ ).isSameAs( louis );
What is the difference between the two here: equals and same?
Equals uses the .equals
method defined for all objects, same uses ==
which works well for primitive types but can lead to surprises for reference types (i.e. objects).
To follow along, tweak your IDE. Make sure you have the code template to create a test method. Note that the template adds a fail line, which should be disabled once the test and the business code agree. |
5.2. String Containment
Student student = ....
assertThat( student.toString() )
.isNotNull() (1)
.doesNotContainIgnoringCase("student{") (2)
.contains( "Harry", "Potter", "1980-07-31", "12345"); (3)
1 | Ensure that the next test can inspect a String without tripping over a NPE. |
2 | Auto generated is too simple. Such autogenerated string typically starts with "Student{" for NetBeans IDE. |
3 | The test ensures that the birth date is contained, and 12345 is Harry’s student number. |
How the string is formatted does not matter for this test to pass. It only requires the shown strings in any order.
5.3. Collection Containment
For collections there are numerous tests. Collections in this context includes everything Iterable, making it very powerful as can be seen in the Javadoc page of Iterable by following the link in this sentence. Look at the sub-interfaces and the implementing classes.
List<Professor> professors = List.of( SNAPE, DUMBLEDORE, MCGONAGALL ); (1)
List<Professor> teachesTransfiguration = professors.stream()
.filter( p -> p.teaches( "Transfigurations" ) ).collect( toList() );
assertThat( teachesTransfiguration )
.isNotNull() (2)
.hasSizeGreaterThanOrEqualTo( 2 ) (3)
.contains( DUMBLEDORE, MCGONAGALL ); (4)
1 | Assume this to be in the business code or SUT. |
2 | Not really required by the test but a good tip anyway, because it ensures that the test does not trip over an NPE, which might be confusing. |
3 | The following contains test makes this assertion redundant, only here for demonstration purposes. |
4 | because we want these two at least. |
// List<Student> hogwartsStudents = ...; (1)
List<Student> hogwartsStudents = List.of( HARRY, HERMIONE, RON ); (1)
assertThat( hogwartsStudents )
.isNotNull() (2)
.hasSizeGreaterThanOrEqualTo( 3 ) (3)
.extracting( s -> s.getStudentNumber() ) (4)
.containsExactlyInAnyOrder( 12346, 12345, 12347 ); (5)
1 | Assume that the business code produces such list. |
2 | Mostly self (test) protect. |
3 | Just to show that you can test for not empty, but also exact size, greater etc. This assertion is actually made redundant by the test in 5. |
4 | Extract a feature out of each student using a lambda. Its function should be obvious (for student s get the student number). |
5 | Then test the list of extracted values for containment. Note that in this case we do not care about the (iteration) order of the collection. |
AssertJ has many more containment tests, to test presence of all, any, exactly, or exactly but without a specific order.
Because adding a Non-Null test is easy and cheap in the fluent style of AssertJ, it can be beneficial to always do that, so your test can show it as a failure instead of tripping over an unexpected NPE. It more or less self-protects the test. |
5.4. Assert Exceptions
In most business processes you want to avoid exceptions, but when they are expected, they must be thrown by the code under test, so that too needs to be tested.
There are three cases:
-
You are not expecting an exception and do not (want to) care about it.
Then let it simply occur and if it is a checked exception make your test method throw it. -
You want specific code not to throw an exception and your want to test for that.
Wrap the suspect code in a lambda and invoke it usingassertThatCode( suspectCode )
. -
You want a specific exception to be thrown under specific a circumstance.
Wrap the exception causing code in a lambda and catch and inspect the resulting exception usingassertThatThrownBy( causingCode )
.
In AssertJ the exception testing helper have a format that deviates from the assertThat().someCheck(…)
style.
This inconsistency has to do with the way the exceptions causing code must be called, and cannot easily be avoided.
We propagate one form, declaring a lambda first and use that as the parameter to exception asserter.
5.4.1. Ignore or pass on
In case you are not interested in an exception in your test, but it is a checked exception, simply declare your test method to throw it.
@Test
public void fileUsingMethod() throws IOException { (1)
Files.lines(Path.of ("puk.txt") ); (2)
}
1 | This code potentially throws an IOException , but you are not interested in testing the exception. If it occurs,
let the caller (Test Runner) deal with this unexpected situation. The IOException is an example. |
2 | This is the method that throws the checked exception. This is an example. Normally it should be a business method. |
5.4.2. Exception NOT wanted.
If you want the check for an exception NOT to occur when invoking a code sequence, isolate the sequence
in a lambda expression of the form ThrowingCallable code =() → { suspectCode(); }
.
ThrowingCallable is a Functional interface and is part of AssertJ.
Student draco = new Student("Draco", "Malfoy", LocalDate.of (1980,6,5));
ThrowingCallable code = () -> {
hogwarts.addStudent( draco ); (1)
};
assertThatCode( code )
.as( "draco should be accepted to make the adventures possible")
.doesNotThrowAnyException();
1 | Is the only code that is checked for exceptions. This isolates the "suspect" code from any other code that may cause issues. |
Sometimes you may have the situation that exceptions appear to come out of the blue, as in you have no idea what causes it and the stack trace is not very helpful either. In such cases, use this test approach to isolate the problematic code. |
5.4.3. Exception needs to occur.
When you want your business code to throw an exception, wrap that business code (the method invocations) in a lambda expression,
in the same way as in the previous paragraph, then pass that code
to the exception assert method.
@Test
public void addIllegalProfessor() {
var malfoy = new Professor( "Lucius", "Malfoy", LocalDate.of( 1953, 10, 9 ) ); (1)
ThrowingCallable code = () -> { (2)
hogwarts.addProfessor( malfoy );
};
assertThatThrownBy( code )
.isInstanceOf( Exception.class) (3)
.isExactlyInstanceOf( IllegalArgumentException.class) (4)
.hasMessageContainingAll( "should","teach"); (5)
// fail( "addIllegalProfessor completed succesfully; you know what to do" );
}
1 | Someone that knows his classics understands that this crook can’t be a professor at Hogwarts. |
2 | The lambda defines the throwing code. org.assertj.core.api.ThrowableAssert.ThrowingCallable is the functional interface for this purpose. |
3 | Sometimes it is good to be a bit relaxed on the exception type like in this line. |
4 | Or you need to be quite specific. You need only one, so choose either line 2 or 3. This is just an illustration. |
5 | You might want to inspect the message for keywords. |
In this fluent style you can check many more things. See the AssertJ user guide and API for that.
The general advise is to have only one (1) assert per test method. This makes the test method very focused. Stick to this rule and do not test unrelated features. Also note that when a failure occurs (a test fails or an exception is thrown), the rest of the test is not executed anymore, and this will therefor obscure further asserts in the same test method. |
Exercise Minibar
Exceptions in a Mini Bar
The narrative of this exercise is a silly story, but will help you test driven develop handling and throwing of exceptions. It is a simplified version of a minibar, which is a self service in some hotels. Maybe we will revisit it in later weeks with some refinements. We are developing code (classes) for a carnival simulations, that throw exceptions to make thins more lively.
The important parts here are:
-
A
Stock
from which you canBeer draw(double)
and that can become empty and throws an exception when it can’t supply the requested volume. The exception is anEmptyStockException
and is a checked exception. You may have seen that too, the tap will sputter. -
A
Beer
has a volume, specified in the constructor as double. Think of it as the glass or mug. A getter for the volume (double) and a constructor that takes the volume. -
A
Guest
that drinks the beer, but can get drunk. This happens when the drinker reaches his capacity. A constructor that specifies the capacity, an additional field that holds the fill, and aGuest drink( Beer )
method that adds the volume of the beer to the fill and returns the Guest, so the guest can do a merryguest.drink(b).drink(b).drink(b);
.
The exception thrown should be an unchecked exception namedDrunkenException
. You may have observed that in the wild.You may have to modify the void return type of Guest.drink(..) from void to Guest.
TDD to achieve that:
-
Test1: The stock can be used when not empty. It should not throw an exception. Assert that the exception is not thrown.
-
Test2: When you draw a volume, getLeft() returns a lower value. Ignore the exception, because you set it up with plenty stock. Test that the volume is reduce by the amount drawn.
-
Test3: an overdraft on the stock throws a checked exception, an
EmptyStockException
when the stock can’t supply the requested volume. -
Test4: The drinker can drink the beer. Give him a civil amount, that should not cause issues.
-
Test5: However, when he is full, he throws a
DrunkenException
.
Convert the above in test scenarios and develop each of the classes and methods.
Use the class and method names as given.
6. Soft Assertions
Sometimes you have a pressing need to assert more than one thing in a test method, because two or more values always come in pairs and setting stuff up and dividing it over multiple methods would make the test less readable, which is bad.
However, having multiple asserts in one method is bad style, because the first problem that occurs is the only failure that will be reported by the test. You could say that the first failure dominates or monopolizes the test method.
However the rule above makes writing tests a bit awkward. In particular when there are multiple aspects to be tested on the same result object. In that case a soft assertion may be helpful. A soft assertion will fail "softly", meaning that the failure is recorded, but the test can still continue to verify another aspect. The soft assertion will collect all failures and will report them after the soft assertion is closed.
void testGetResult(){
// some setup
Optional<KeyValue<Integer,Double>> = new gf(student).gradeFor("PRC2");
SoftAssertions.assertSoftly( softly -> { (1)
AbstractMap.SimpleEntry<Integer, Double> result = gf.getResult();
softly.assertThat( result )
.extracting( "key" )
.isEqualTo( student.getStudentId() );
softly.assertThat( result )
.extracting( "value" )
.isEqualTo( grade );
}); (2)
}
1 | Starts the lambda that encloses the soft assertions. Softly serves as the collector of the failures |
2 | Closes the sequence of soft assertions. This is the spot where SoftAssertion will report the the failures, if any. |
In the example, the key and value in the KeyValue pair are related, making a soft assertion applicable.
Exercise Simple Queue
Simple queue
You have to write your own simple queue implementation using nodes as in the diagram. Simply delegating the work to an already existing queue or (ab)using some kind of list implementation from the JDK is not legal for this exercise.
In this exercise you will develop a simple queue class called
queue.SimpleQueue
to get acquainted with the TDD rhythm.
We will describe the requirements of the queue as a short Functional Requirement, recognizable as FRx where x is some number. Where needed, we will add Non-Functional requirements as well (recognizable as NFx). Each functional requirement leads to a test.
A queue is a First In First Out (FIFO) data structure.
We will describe the requirements quite formally here, in the hope to make them very clear. Other exercise may require that you do this formalization in your head, because the requirements may be described in a more relaxed style.
The simple queue you are to develop is a so called single linked queue.
Internally you have Node objects that carry an item and a link to the next node.
The last node’s next is null
.
The LinkedList has two fields of type node
-
head, which points at the place where you would take or peek elements from the queue. The head looks at the first element that will come off.
-
tail, points at the place where the next new element is to added.

-
NF1: All fields of the queue class are
private
. -
NF2: The queue shall use a linked node structure.
-
NF3: None of the fields in the SimpleQueue objects shall be of some array type.
-
FR1: The queue shall be declared generic, with type token
<E>
.Test: Create a JUnit test class in the package
queue
and name the test classSimpleQueueTest
.
Create a test method namedqueueIsGeneric()
. Let the test method create two instances of the SimpleQueue, one with items of generic type<String>
, the other of type<Integer>
. -
FR2: queue can be created with a no-args constructor.
This means that there should be a constructor so that you can donew SimpleQueue()
.
If a class does not yet have any constructors, the compiler will add a default constructor,[2] which is a no-arguments constructor.
An SimpleQueue has a method calledisEmpty()
taking no parameters, that reports that the queue contains no elements. Immediately after instantiatingisEmpty()
reports true.Test that the queue object is empty after creation.
-
FR3: The method
void put(E e)
adds an element to the queue. If the queue was empty before, the object e will appear as the first element in the queue. Immediately after a put, the queue is not empty.Test: Call it
putMakesNotEmpty
. Create two SimpleQueue objects, one for String and one for Integer. Use the put method to add one element to each of the queues. Each queue should report not empty, i.e.isEmpty()
returns false. -
FR4: The queue has an
E peek()
method, that returns the first element of the queue without removing it from the queue.
If there is only one element in the queue, that element should be returned.Test: Create a SimpleQueue for each of the types String and Integer. Put an element of each type in the queue. Create a String named
expectedString
with some content of your liking. Invoke the peek() method on each queue and assert that each returns the correct object, the one that you put in. You should useassertThat(q.peek()).isSameAs( expectedString )
for the String. for the integer it is similar. -
FR5: The queue has an E take() method that returns and removes the first element of the queue. If there is only one element in the queue, the queue should become empty.
Test: Test that the take method returns the correct element and reduces the size of the queue. A queue should be empty after one put and one take.
-
FR6: The queue can contain multiple elements. The values that are returned by take are in the same order as the order in which the elements have been put on the queue.
Test: Create a queue for strings. Put multiple elements to the queue, one after the other. Use an an array of Strings as source of data:
String[] values= { "Hello", "World", "And", "Some", "Bla", "Di", "Bla" };
.
Use a for-each loop to put the values in the queue.
Create anArrayList<String>
namedreturns
and in a while loop take all elements from the queue and put them in the list.
UseassertThat( returns ).containsExactly( values );
to verify the correct behavior of the queue. Also ensure that the queue reports empty after all takes. -
FR7: The queue should implement the Iterable interface.
Test: Copy the previous test, but instead of using an intermediate list to collect the returned values, use the fact the an Iterable can be tested for it’s content as is.
Inside the Iterator implementation that you return you should use a cursor or current
that maintains where the iterator has been. Initialize it to point to the (dummy) head of the queue.boolean hasNext()
checks that there is another element to be visited,T next()
moves the cursor and returns the element it is 'looking' at.
Ensure that the iterator does not destroy the queue. You can test that by using iterating twice, once to print the content (for-each loop), the 2nd time to check that the content is still there in theassertThat(…).contains(…)
part.
for ( String string : q1) {
System.out.println( string );
}
assertThat( queue ).containsExactly( values );
-
FR8: The queue should throw an
IllegalStateException
when a peek or take operation is done while the queue is empty. So far we played nicely with the queue. Now we want to make sure that the queue can protect itself from abuse, so that the queue object stays in a correct state.Test: Use
assertThatThrownBy(code)
, to ensure that code that tries to peek or take.
Write 2 tests, one for peek and one for take. Try to avoid code duplication in the queue class.
ThrowingCallable code = () -> { queue.peek(); };
The queue is now feature complete.
7. Assumptions
There are test cases that are only useful under certain conditions:
-
The test is only meaningful on a specific Operating System.
-
The test needs a database connection and only executes when available.
-
A file must be present.
-
A 'slow' test flag is set in some file, which only executes the test when that flag is set. Used to avoid slow tests in the normal write-compile-test cycle.
For this you can use Assume.
There are two variants relevant:
You can use the Assumption test in the setup methods (the one with @BeforeXXX
), to completely disable all tests in one test class,
instead of having each test doing the assumeXXX
test. A test that has been canceled because of a failing assumption will
appear as a skipped test, similar to a disabled test with @Disabled
.
If you want to disable a whole test class, Do the assumption in a @BeforeAll annotated static setupClass() method. |
The JUnit 5 Assumtions are effective, but can only test for true or false, so you need to feed it a boolean expression. Look at the API to see all features. But simple may be just perfect here.
@Test
void testOnlyOnDeveloperWorkstation() {
assumeTrue("DEV".equals(System.getenv("ENV")), "Aborting test: not on developer workstation");
// remainder of test is only executed if on developer machine
}
You guessed it, AssertJ Assumptions are richer. In most cases,
the assumption has the same form as an assert that, as in replace assert with assume and you are done.
So any assertThat(…)
that you already have can simply be turned into an assumeThat(…)
.
If nothing else, using assertj may make your tests more readable and have a consistent look and feel.
File f = new File(Path.of("testData.dat"));
assumeThat(f)
.as("Test data file not available, skipping test")
.exists();