In this section, we will discuss the CORRECT acronym, as proposed by Jeff Langr, Andy Hunt, and Dave Thomas, in Pragmatic Unit Testing in Java 8 with JUnit, 2015.
Your unit tests can help you prevent shipping some of the defects that often involve boundary conditions-the edges around the happy-path cases where things often go wrong.
The CORRECT acronym can help you think about the boundary conditions to consider for your unit tests.
For each criteria, consider the impact of data from all possible origins—including arguments passed in, fields, and locally managed variables.
Then seek to fully answer the question: What else can go wrong?
We often build methods expecting that the inputs we receive will comply with a certain format.
For example, a URL usually follows the form (Wikipedia):
URL = scheme ":" ["//" authority] path ["?" query] ["#" fragment]
where the authority component divides into three subcomponents:
authority = [userinfo "@"] host [":" port]
Valid URLs include:
http://example.com
http://example.com/questions/3456/my-document
http://example.com/questions#title
http://example.com?query=question
If we have a method that processes and extracts the host of a URL, we could test the following boundary conditions.
:example.com
)http//example.com
)http://
)Consider the methods in the class below, which was designed to extract domains from emails.
import java.util.*;
import java.util.regex.*;
public class Email {
public static List<String> getDomains(List<String> emails) {
return emails.stream()
.map(Email::getDomain)
.toList();
}
public static String getDomain(String email) {
Matcher matcher = Pattern.compile(".+?@(.+)")
.matcher(email);
matcher.find();
return matcher.group(1);
}
}
Think of 3 boundary conditions related to input conformance for the methods of the Email
class and implement test methods that check them.
In the test class below, you will find only methods that test the happy path.
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class EmailTest {
@Test
void shouldReturnGmailDotCom() {
String domain = Email.getDomain("example@gmail.com");
assertThat(domain).isEqualTo("gmail.com");
}
@Test
void shouldReturnUnibzDotIt() {
String domain = Email.getDomain("example@unibz.it");
assertThat(domain).isEqualTo("unibz.it");
}
@Test
void shouldReturnUnibzDotItAndGmailDotCom() {
List<String> domains = Email.getDomains(List.of("example@gmail.com", "example@unibz.it"));
assertThat(domains).contains("gmail.com", "unibz.it");
}
}
To run this code, you can use the following pom.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>it.unibz.inf</groupId>
<artifactId>lecture-testing</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.22.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
</plugins>
</build>
</project>
The order of data, or the position of one piece of data within a larger collection, represents a CORRECTness criterion where it’s easy for code to go wrong.
Let's suppose we wrote a method that returns a list of words, in alphabetical order, that occur more than once in an input text.
Given this input
"I love every kind of pizza, but what I love the most is pineaple pizza."
our method should return
["I", "love", "pizza"]
Given this other input
"Sometimes science is more art than science, but art is never science."
it should return
["art", "is", "science"]
If we were to implement the methods as show below, we would fail the second test because of the order in which repeated words appeared in the text input.
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class Ordering {
public static List<String> findRepeatedWords(String text) {
return getWordCountMap(text)
.entrySet()
.stream()
.filter(entry -> entry.getValue() > 1)
.map(Map.Entry::getKey)
.toList();
}
private static Map<String, Long> getWordCountMap(String text) {
String[] words = text.replaceAll("\\.|,", "").split(" ");
Map<String, Long> count = new LinkedHashMap<>();
for (String word : words) {
long value = 0;
if (count.containsKey(word))
value = count.get(word);
count.put(word, value + 1);
}
return count;
}
}
Note that, in the first phrase, repeated words appeared in alphabetical order in the original text, while in the second case, they did not.
Suppose that getDomains()
should return domains in alphabetical order. Write methods that verify such a behavior, altering the original source code as needed.
Make sure you consider at least one boundary condition related to input ordering.
When you use Java’s built-in types for variables, you often get far more capacity than you need.
For example, if you represent a person’s age using an int
,
you’d be safe for mostly safe, but things can still go wrong.
Consider this Person class.
public class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
To test these two domain rules regarding the age
field, we can write the following tests:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
class PersonTest {
@Test
void personConstructorShouldFailOnNegativeAge() {
Throwable t = catchThrowable(() -> {
Person p = new Person("John", -2);
});
assertThat(t).isInstanceOf(IllegalArgumentException.class);
}
@Test
void personConstructorShouldFailOnExcessivelyLargeAge() {
Throwable t = catchThrowable(() -> {
Person p = new Person("John", 200);
});
assertThat(t).isInstanceOf(IllegalArgumentException.class);
}
}
Excessive use of primitive datatypes is a code smell known as primitive obsession primitive.guru.
Signs and Symptoms:
Reasons for the Problem:
Like most other smells, primitive obsessions are born in moments of weakness. “Just a field for storing some data!” the programmer said. Creating a primitive field is so much easier than making a whole new class, right? And so it was done. Then another field was needed and added in the same way. Consequently, the class became huge and unwieldy.
Primitives are often used to “simulate” types. Instead of a separate data type, you have a set of numbers or strings that you use to form the list of allowable values for some entity. Easy-to-understand names are then given to these specific numbers and strings via constants, which is why they’re widely used.
Another example of poor primitive use is field simulation. The class contains a large array of diverse data and string constants (which are specified in the class) are used as array indices for getting this data.
Consider the Product
class below:
public class Product {
String Name;
String manufacturer;
double price;
public Product(String name, String manufacturer, double price) {
Name = name;
this.manufacturer = manufacturer;
this.price = price;
}
}
When testing a method, consider:
A web app that displays a customer’s account history might require the customer to be logged on. Shifting your car’s transmission from "Drive" to "Park" requires you to first stop.
When you make assumptions about any state, you should verify that your code is reasonably well-behaved when those assumptions are not met.
Imagine you’re developing the code for your car’s microprocessor-controlled transmission. You want tests that demonstrate how the transmission behaves when the car is moving versus when it is not.
@Test
public void remainsInDriveAfterAcceleration() {
transmission.shift(Gear.DRIVE);
car.accelerateTo(35);
assertThat(transmission.getGear()).isEqualTo(Gear.DRIVE);
}
@Test
public void ignoresShiftToParkWhileInDrive() {
transmission.shift(Gear.DRIVE);
car.accelerateTo(30);
transmission.shift(Gear.PARK);
assertThat(transmission.getGear()).isEqualTo(Gear.DRIVE);
}
@Test
public void allowsShiftToParkWhenNotMoving() {
transmission.shift(Gear.DRIVE);
car.accelerateTo(30);
car.brakeToStop();
transmission.shift(Gear.PARK);
assertThat(transmission.getGear()).isEqualTo(Gear.PARK);
}
The preconditions for a method represent the state things must be in for it to run.
The postconditions for a method represent the state that you expect the code to make true.
You can uncover a good number of potential defects by asking yourself:
“Does some given thing exist?”
For a given method that accepts an argument or maintains a field, think through what will happen if the value is null, zero, or otherwise empty.
Some existence tests for our previous Email
class could look like this.
@Test
void shouldFailOnNull(){
Throwable t = catchThrowable(() -> {
Email.getDomain(null);
});
assertThat(t).isInstanceOf(IllegalArgumentException.class);
}
@Test
void shouldFailOnEmpty(){
Throwable t = catchThrowable(() -> {
Email.getDomain("");
});
assertThat(t).isInstanceOf(IllegalArgumentException.class);
}
Cardinality is a more general case of existence.
In this case, you’re looking at more-specific answers than “some” or “none.” Still, the count of some set of values is only interesting in these three cases:
0
1
n
(more than one)Some refer to this as the 0-1-n rule.
Suppose you maintain a list of the top ten food items ordered in a restaurant. Every time an order is taken, you adjust the top-ten list, which you display in your restaurant's app in real time.
The notion of cardinality can help you derive a list of things to test out, for example:
There are several aspects of time to keep in mind.
Some interfaces are inherently stateful.
You expect login()
to be called before logout()
, open()
before read()
, read()
before close()
, and so on.
To test such interfaces, consider what happens if:
Just as order of data matters, the order of the calling sequence of methods matters!
Sometimes, we have to think about how long our code can take to execute, e.g.,
For instance, suppose your method makes an HTTP request to get some data from a web API. You may want to test scenarios in which:
Every rare once in a while, time of day matters, perhaps in subtle ways. A quick quiz:
True or false? Every day of the year is 24 hours long.
It depends.