Testing static methods
Table of contents
How can we test functionality that makes use of static methods?
The following test invokes our game.
package demo;
import org.junit.jupiter.api.Test;
public class AppTest {
@Test
public void shouldDisplayTheDiceRolled() {
App.main( null );
}
}
This test will print something similar to the following to the output.
[12:34:56] Game started
[12:34:56] Please roll the 🎲
[12:34:56] You rolled 4 and 2
It is hard to verify that the two numbers printed are actually the values that were rolled.
The simplest way to make this example testable is to use objects. In a nutshell we will need to create and use test doubles to control and verify that our game is behaving as expected.
Refactoring
Note that it is hard to refactor code that does not have tests and also maintaining the code functionality intact.
Move the game into a separate method
playGame()
.package demo; public class App { public static void main( final String[] args ) { playGame(); } public static void playGame() { Display.print( "Game started" ); Display.print( "Please roll the 🎲" ); final int a = Dice.roll(); final int b = Dice.roll(); Display.printf( "You rolled %d and %d", a, b ); } }
This will not solve the problem, but it’s one small step in the right direction.
Later on, we need to work with test doubles. Given that we cannot change the signature of the
main()
method, we need to create a new method and use this one.package demo; import org.junit.jupiter.api.Test; public class AppTest { @Test public void shouldDisplayTheDiceRolled() { App.playGame(); } }
The test will still print something to the console, which is hard to assert.
[12:34:56] Game started [12:34:56] Please roll the 🎲 [12:34:56] You rolled 3 and 3
Predict the next
roll()
outcomeOne of the challenges we face in testing this application is that we cannot predict the next roll outcome using the current
Dice
class, as theDice
class uses a random number generator.Use weighted dice for testing
package demo; public class WeightedDice { public static int roll() { return nextRollValue; } public static int NEXT_ROLL_VALUE = 6; }
The
WeightedDice
does not use random number generator. Instead, it returns the value ofNEXT_ROLL_VALUE
. Using this version of the dice will allow us to control the behaviour of the game.How can we us the
WeightedDice
?The
playGame()
method is calling theDice
’sroll()
. If we need to swap this we need pass theDice
as a parameter to theplayGame()
method. If we want to pass theDice
as a parameter to theplayGame()
method, then we need to convert theDice
to an object.Make
Dice
an objectSomehow, we need to control what the dice rolls are and what is it being printed. We can do that be controlling the
Dice
output and capturing theDisplay
inputs.package demo; import java.util.Random; public class Dice { public int roll() { return randomGenerator.nextInt( 6 ) + 1; } public final Random randomGenerator = new Random(); }
Note that the variable name was changed from
RANDOM_GENERATOR
torandomGenerator
too.static
fields are written upper case and use underscore to delimit words. Properties and all methods are usually written in camelcase.Use the
Dice
object in theplayGame()
method.final Dice dice = new Dice(); final int a = dice.roll(); final int b = dice.roll();
Complete example.
package demo; public class App { public static void main( final String[] args ) { playGame(); } public static void playGame() { Display.print( "Game started" ); Display.print( "Please roll the 🎲" ); final Dice dice = new Dice(); final int a = dice.roll(); final int b = dice.roll(); Display.printf( "You rolled %d and %d", a, b ); } }
The above example is creating the
Dice
object. The next step would be to pass an instance of theDice
class to theplayGame()
method.Provide a
Dice
instance to theplayGame()
methodpackage demo; public class App { public static void main( final String[] args ) { final Dice dice = new Dice(); playGame( dice ); } public static void playGame( final Dice dice ) { Display.print( "Game started" ); Display.print( "Please roll the 🎲" ); final int a = dice.roll(); final int b = dice.roll(); Display.printf( "You rolled %d and %d", a, b ); } }
Note that now we need to provide an instance of
Dice
in the test too.package demo; import org.junit.jupiter.api.Test; public class AppTest { @Test public void shouldDisplayTheDiceRolled() { final Dice dice = new Dice(); App.playGame( dice ); } }
Convert the
WeightedDice
to an object too.If we want to swap the
Dice
with aWeightedDice
during testing, we need to convert the latter to an object too.package demo; public class WeightedDice { public int roll() { return nextRollValue; } public int nextRollValue = 6; }
How can we us the
WeightedDice
?⚠ The following example does not compile!!package demo; import org.junit.jupiter.api.Test; public class AppTest { @Test public void shouldDisplayTheDiceRolled() { /* The WeightedDice is not a subtype of Dice!! */ final Dice dice = new WeightedDice(); App.playGame( dice ); } }
Dice
andWeightedDice
are two different types. TheWeightedDice
class is only used for testing and can inherit from theDice
class. Inheritance will be covered in depth in the inheritance section.public class WeightedDice extends Dice {
Complete example
package demo; public class WeightedDice extends Dice { @Override public int roll() { return nextRollValue; } public int nextRollValue = 6; }
Now the
WeightedDice
can be used instead of theDice
. The test will now work.package demo; import org.junit.jupiter.api.Test; public class AppTest { @Test public void shouldDisplayTheDiceRolled() { final WeightedDice dice = new WeightedDice(); dice.nextRollValue = 2; App.playGame( dice ); } }
Note that irrespective how many times we run the above test, the dice values will always be
2
. This is great for testing.[12:34:56] Game started [12:34:56] Please roll the 🎲 [12:34:56] You rolled 2 and 2
The
WeightedDice
is also referred as a test double.
We can continue refactoring the application and provide a test double for the Display
and verify that the right message is being printed.
Martin Fowler talks in more depth about this topic in his StaticSubstitution article.
What does static
mean?
When a class member (field or method) is marked static
, that means that this member belongs to the class and not to any instance. When the roll()
was static
, we were able to call it through the Dice
class name.
Dice.roll()
Methods that do not have any state, can be safely marked as static
, but use this carefully. As we saw above, it is hard to test functionality that depends on other static methods.
The Math
class is a good example.
package demo;
public class App {
public static void main( final String[] args ) {
final int a = 7;
final int b = 3;
final int m = Math.max( a, b );
System.out.printf( "%d is the largest between %d and %d%n", m, a, b );
}
}
The max()
method is stateless and thus no need to make it an instance method.
Consider the following example.
package demo;
public class App {
public static void main( final String[] args ) {
final Math nullVariable = null;
final int m = nullVariable.max( 7, 3 );
System.out.printf( "%d is the largest number%n", m );
}
}
Will the above example throw a NullPointerException
?
The variable nullVariable
is of type Math
and is set to null
. Invoking the max()
method should throw a NullPointerException
but it does not. The above code will work.
7 is the largest number
Why this works and does not throw a NullPointerException
?
The max()
method is static
which means it does not work with the Java heap. static
members are part of the class metadata, which is not saved in the Java heap, but elsewhere. The class metadata is loaded once (per classloader), which include all static
fields and all methods definition. static
methods cannot access the object’s state in the same way the non-static (or instance) method do, thus static
methods do not interact with the Java heap.
When the max()
method is called in the above context, the max()
method is simply loaded in the Java stack and executed and no interaction with the Java heap is made during this process. When invoking an instance method, the object’s state is also involved in the process, which is found in the Java heap.
The variable nullVariable
is of type Math
and can access any member that this type defines. The Math
class has the max()
static
method which can be access or through the Math
class name or through a variable of type Math
.
How do static
fields behave?
The Java API has a Point
class that can be used to represent a point on a cartesian plane.
package demo;
import java.awt.Point;
public class App {
public static void main( final String[] args ) {
final Point a = new Point( 1, 2 );
final Point b = new Point( 3, 4 );
System.out.printf( "Point a: %s%n", a );
System.out.printf( "Point b: %s%n", b );
}
}
The above example creates two points and prints their state.
Point a: java.awt.Point[x=1,y=2]
Point b: java.awt.Point[x=3,y=4]
Say that we need to capture the persons’ name to be then added to an online fictitious address book. Consider the following example.
package demo;
public class Person {
public static String name;
}
The Person
class has one static
field called name
.
package demo;
public class App {
public static void main( final String[] args ) {
final Person aden = new Person();
aden.name = "Aden";
System.out.printf( "Person name (aden): %s%n", aden.name );
}
}
The above creates an instance of the Person
class and print it.
Person name (aden): Aden
Works!! Let create a second instance of the Person
class and print both instances.
package demo;
public class App {
public static void main( final String[] args ) {
final Person aden = new Person();
aden.name = "Aden";
final Person jade = new Person();
jade.name = "Jade";
System.out.printf( "Person name (aden): %s%n", aden.name );
System.out.printf( "Person name (jade): %s%n", jade.name );
}
}
The above will print the following.
Person name (aden): Jade
Person name (jade): Jade
Oops!! What went wrong? Why we are able to print two different points but not able to print two different persons?
Let’s compare the two classes.
The Point
class has two properties as shown next.
package java.awt;
/* The class was heavily simplified from brevity */
public class Point {
public int x;
public int y;
}
Different from our Person
, the two properties shown above do not make use of the static
modifier.
package demo;
public class Person {
public static String name;
}
A static
field is not part of the object and thus it is not part of the Java heap. The static
fields are saved together with the class metadata, which is not saved in the Java heap, but elsewhere. The class metadata is loaded once (per classloader), which include all static
fields and all methods definition.
Therefore, there is only one copy of the static
field, name
. If one modifies a static
field, all variables will be affected. IntelliJ suggests refactoring the code and access the static
field through the class name.