1. Parameterized tests
You will often see that test methods look a lot like each other. As an example: In the fraction exercise, in most test methods you have two inputs and one or two results, then an operation is done followed by some assertion, often of the same kind. This quickly leads to the habit of copy and waste programming. Many errors are introduced this way: You copy the original, tweak the copy a bit and you are done. Then you forget one required tweak, because they are easy to miss, but you do not notice it until too late.
Avoid copy and waste at almost all times. It is NOT a good programming style. If you see it in your code, refactor it to remove the copies, but instead make calls to one version of it. This will make you have less code overall. Coding excellence is never measured in number of code lines, but in quality of the code. Think of gems. They are precious because they are rare. The copy and waste problem can even become worse: When the original has a flaw, you are just multiplying the
number of flaws in your code. This observation applies to test code just as well. CODE THAT ISN’T WRITTEN CAN’T BE WRONG. |
1.1. Parameterized test, Junit 5 style
Below you see an example on how you can condense the toString()
tests of fraction from 5 very similar test methods into 5 strings containing the test data
and 1 (say one) test method.
Paraphrasing a saying: Killing many bugs with one test.
package fraction;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
import org.junit.jupiter.params.provider.CsvSource;
public class FractionCSVSourceTest {
@ParameterizedTest
@CsvSource( {
//"message, expected, n, d", (1)
"'two thirds', '(2/3)', 2, 3", (2)
"'whole number, '2', 2, 1",
"'one third', '(1/3)', -3, -9",
"'minus two fifths', '(-2/5)', 12, -30",
"'one + two fifths', '(-1-(2/5))', 35, -25"
} )
void fractionOps( String message, String expectedString,
int numerator, int denominator ) { (3)
Fraction f = new Fraction( numerator, denominator );
assertThat( f.toString() )
.as( message )
.isEqualTo( expectedString );
}
}
1 | Adding a comment is always a good idea. You may also want your values aligned for improved readability. |
2 | Parameters are separated by commas, which maybe in the test values. In that case you can demarcate Strings with single quotes inside the csv record string. If you need another separator instead of comma, you can specify that too, see CsvSource API . |
3 | The parameters are passed in as the type(s) provided by the test method’s signature. The Junit-5 framework will (try to) parse the csv record elements accordingly. |
For more details see Junit 5 parameterized tests .
The CsvSource version of parameterized test is the simplest to use and easily understood. It keeps data and the test together, nicely near to each other, so it make it easy to read the tests and data in one glance. They typically fit easily on a screen. Remember the importance of readability of code. That too applies to test code.
We want to make sure that the assignment project is acceptable by the compiler. We create the project ourselves first, making sure it works and then strip our solution. All TDD of course. To make that work, we sometime have to comment out code in the test or in the business part of the code,
because that code is not yet ready (because the student did not write it yet).
In such cases we comment out using two slashes, which both best and easiest. |
Exercise Fraction part 1.
Fraction Part 1, Constructor and normalisation
This is a 3 part exercise.
In this exercise, you are writing a Fraction class test-driven, meaning you write the tests first.
The mathematical properties of fractions are at the end of this exercise, if you need to look them up.
The source code is in English, using the common names fraction, numerator and denominator.
The mathematical representation typically is:
\(\frac{\text{numerator}}{\text{denominator}}\), example: \(\frac{1}{2}\).
In this exercise the tests are predefined, to direct you towards using Parameterized tests. Some the test values are given, some other have to enabled by un-commenting the test value lines. |
Fraction Constructor
-
Write a test that calls the fraction constructor with two distinct ints, say 4 and 5, and then invokes the getters
int getNumerator()
(should produce 4) andint getDenominator()
(should produce 5) to assure that the fraction instance is properly constructed.
Use a ParameterizedTest with cvs source and have the csv records have 5 columns, a message, int a and int b to form fraction \(\frac{a}{b}\), the expected numerator, and the expected denominator. Use soft assertions to test both getters in one test. Add at least the sample values, but be prepared to add more rows. -
Implement the constructor of Fraction with parameters
n
andd
for numerator and denominator respectively. In the constructor, save the parameters in the fieldsnumerator
anddenominator
, the only two fields. Make sure the fields are final. Inside the constructor you are allowed to use local variables. -
Run the tests. If they are still red, improve your code to make them green.
/**
* Test getters for numerator and denominator.
* Use SoftAssertions.assertSoftly to always test both getters
* even if the first assert fails.
* @param a num in frac
* @param b denom in frac
* @param num expected
* @param denom expected
*/
//@Disabled("Think TDD")
@ParameterizedTest
@CsvSource( {
// message, a,b, num, denom
"half,1,2,1,2",
"one third,2,6,1,3",
"minus one half,9,-18,-1,2",
} )
void testGetters( String message, int a, int b, int num, int denom ) {
Fraction f = new Fraction( a, b );
SoftAssertions.assertSoftly( softly -> { (1)
softly.assertThat( f.getNumerator() )
.as( message + " numerator" ) // to tell fails apart
.isEqualTo( num );
softly.assertThat( f.getDenominator() )
.as( message + " denominator" )
.isEqualTo( denom );
} ); (2)
// fail( "method testGetters reached end. You know what to do." );
}
1 | Start sequence of soft assertions. Notice the use of the softly object. |
2 | End of seqiencs of assertions. This reports all found failing assertions. |
Ensure that the resulting Fraction is
normalised. Fractions like \(\frac{2}{4}\) should result in \(\frac{1}{2}\).
By dividing numerator \(a\) and denominator
\(b\) before construction of a fraction by \(g = gcd(a,b)\) you produce a
fraction with the same mathematical value, called a normalised fraction.
Normalisation should take place in the constructor.
|
-
Write test for ToString Create a fraction from the inputs of the CsvSource and check that it is equal to the expected string.
-
Implement Create a toString() method, by modifying the IDE generated on such that it outputs like this:
(1/2)
for \(\frac{1}{2}\),(-1/3)
\(\frac{-1}{3}\), and (-3-(1/6)) for \(-1-\frac{1}{6}\) which is equal to \(\frac{-7}{6}\).
You are at 1/3 of the fraction exercise.
The properties of fractions, for reference
The mathematical properties of fractions in a typical Java implementation.
Let \(f_1\) be a fraction with numerator \(a\) and denominator \(b\) thus: \(f_1=\frac{a}{b}\). And \(f_2\) a fraction with numerator \(c\) and denominator \(d\), thus \(f_2=\frac{c}{d}\).
Greatest Common Divisor
The function \(\gcd(p,q)\) is called Greatest Common Divisor. With parameters \(p\) and \(q\) it produces integer value \(g\), which is the greatest integer value that divides both \(p\) and \(q\) evenly (divide with zero remainder). Method is given below.
-
Normalisation By applying idempodence, dividing numerator \(a\) and denominator \(b\) before construction of a fraction by \(g = gcd(a,b)\) produces a fraction of same value, called a normalised fraction. With \(g =gcd(a,b)\) \(f_1 = \frac{a}{b} = \frac{a/g}{b/g}\)
-
Negation The negative of a fraction is the same as a fraction with the numerator negated. \( -f_1 = -\frac{a}{b} = \frac{-a}{b} \)
-
Move sign to numerator When at construction time of a fraction the denominator is negative, negate both numerator and denominator. \( f_1=\frac{a}{-b} = \frac{-a}{b}\)
-
inverse The inverse of a fraction is the same as the swapping places of numerator and denominator. Note that multiplying a fraction with its inverse is \(1\), by applying the rules idempodence and unity. \(\frac{1}{f_1} = \frac{1}{(\frac{a}{b})} = \frac{b}{a}\)
-
Addition Add two fractions. \(f_1+f_2 = \frac{a}{b}+\frac{c}{d}=\frac{a\times d + c\times b}{b\times{}d}\)
-
Subtraction Subtraction of two fractions \(f_1\) and \(f_2\) is the same as addition of \(f_1\) and the negation of \(f_2\). \(f_1-f_2 = \frac{a}{b}-\frac{c}{d}=\frac{a}{b}+\frac{-c}{d}\)
-
Multiplication \( f_1\times{}f_2 = \frac{a}{b}\times\frac{c}{d}=\frac{a\times{}c}{b\times{}d}\)
-
Division Dividing fraction \(f_1\) by \(f_2\) is the same as multiplication \(f_1\) with the inverse of \(f_2\).
\( f_1/f_2 = \frac{a}{b}/\frac{ c }{ d }=\frac{a}{b}\times \frac{ d }{ c } \)
/**
* Compute Greatest Common Divisor. Used to normalize fractions.
*
* @param a first number
* @param b second number, gt 0
* @return greatest common divisor.
*/
static int gcd( int a, int b ) {
// make sure params to computation are positive.
if ( a < 0 ) {
a = -a;
}
if ( b < 0 ) {
b = -b;
}
while ( b != 0 ) {
int t = b;
b = a % b;
a = t;
}
return a;
}
1.2. Lookup in a map.
Sometimes there is no easy way to get from a string to some object, or even class.
Examples are that you want to test for the correct type or exception, or supply a method name, but you cannot put that into a string without doing complex
things like reflection which we may only do in later parts of the course. The trick is to use a Map
that maps a string to the object of choice.
The lookup trick might also be applicable when you want to have short values in your test data table, like a message number which is mapped
to the actual longer expected message, or even a message that is a format string and can be used in Assertion.as( formatString, Object… args)
.
static Map<String, Person> emap = Map.of(
"piet", new Person("Piet", "Puk", LocalDate.of(1955-03-18),"M"),
"piet2", new Person("Piet", "Puk", LocalDate.of(1955-03-18),"M"), // for equals test.
"rembrandt", new Person("Rembrandt", "van Rijn", LocalDate.of(1606,7,15),"M"),
"saskia", new Person("Saskia", "van Uylenburgh", LocalDate.of(1612,8,11),"F"),
);
It is particularly applicable to lambda expressions.
Lambdas are special. They have no name and not a very useful toString, and you can’t overwrite them either.
But you can do the reverse: translate a string into a lambda, by using a map. You can then use simple names (strings),
that can be put in a csv record. You could, if you wanted, even use the lambda expression in String form as a key.
Map.of("(a,b)->a+b", (a,b)->a+b );
final Map<String, BiFunction<Fraction, Fraction, Fraction>> ops = (1)
Map.of(
"times", ( f1, f2 ) -> f1.times( f2 ),
"plus", ( f1, f2 ) -> f1.plus( f2 ),
"flip", ( f1, f2 ) -> f1.flip(), (2)
"minus", ( f1, f2 ) -> f1.minus( f2 ),
"divideBy", ( f1, f2 ) -> f1.divideBy( f2 )
);
1 | Note that we use a BiFunction<T,U,R>, with T, U, and R all of the same type: Fraction. This is legal. |
2 | f2 is not used in the right hand side of the arrow. This is legal too. |
@ParameterizedTest
@CsvSource(
{
"'one half times one third is 1 sixth', 'times', '(1/6)',1,2,1,3", (1)
"'one thirds plus two thirds is 1' , 'plus', '1',1,3,2,3",
"'flip' , 'flip', '3',1,3,1,3", (2)
"'one half minus two thirds is' , 'minus', '(-1/6)',1,2,2,3"
} )
1 | The operation name is the second value in the csv record, here times. Note that you can quote strings, but that is not required. |
2 | In the flip operation, the second fraction is ignored, so any legal value is allowed. Here we use the same values for both fractions. |
void fractionOps( String message, String opName, String expected,
int a,int b, int c, int d ) { (1)
// Arrange: read test values
Fraction f1 = frac( a, b ); (2)
Fraction f2 = frac( c, d );
BiFunction<Fraction, Fraction, Fraction> op = ops.get( opName ); (3)
// Act
Fraction result = op.apply( f1, f2 ); (4)
// Assert(That) left out as exercise.
// Use assertThat on the fraction object result
// and check if it has the correct string value.
// Use the message in the as(...) method.
}
1 | The fraction parameters a,b,c, and d are captured from the csvrecord. This makes the parameter list a tad longer, but also more understandable. JUnit 5 csvsource uses the annotation and the signature of the test method and can deal with most common types such as primitive, String and LocalDate (preferably in ISO-8601 format such as '2012-01-14' for the day of writing this). |
2 | The fraction instances are created from a, b, c, and d. |
3 | The operation (op) is looked up in the map. |
4 | Apply the operation, or rather the function and capture the result. |
You can apply the same trick of looking up with enums too, even easier, because the enum itself can translate from String to value, as long as the spelling is exact.
Study the examples above, they might give you inspiration with the exercises coming up and will score you points during the exam.
Exercise Fraction part 2.
Fraction Part 2, Operations
Add the important operations to the Fraction class: times, plus, divideBy and minus.
To multiply two fractions \(\frac{a}{b}\) and \(\frac{c}{d}\) consider the property \(\frac{a}{b}\times\frac{c}{d} = \frac{a\times{}c}{b\times{}d}\) Example: \(\frac{1}{2}\times\frac{3}{4}=\frac{1\times{}3}{2\times{4}}=\frac{3}{8}\).
The methods below can all be tested with one ParameterizedTest and a lookup table, as explained in the previous paragraphs.
Start by implementing the test, and add test data as in the example, but keep the test records commented out until you are about to implement the helper and operation methods.
-
times helper
Implement the helper methodFraction times( int otherN, int otherD )
to multiplythis
fraction with numeratorotherN
and denominatorotherD
of another fraction. The result is a newFraction
-object.-
times Fraction Test and implement the method
Fraction times(Fraction other)
. Re-use (not copy) the helper function.
-
-
Plus helper Test and implement the helper method. It should produce a normalized fraction from the two int values.
Fraction plus( int otherN, int otherD )
to create anew
fraction that is the sum ofthis
fraction andanother
fraction with numerator otherN and denominator otherD. The result is a newFraction
-object. Formula: \(\frac{a}{b}+\frac{c}{d} = \frac{a\times{d}+c\times{b}}{b\times{d}}\)-
plus Fraction now test and implement the method Fraction plus(Fraction other). Re-use the helper function.
-
-
Minus, divideBy. Test and implement minus and divideBy operation. You should re-use the work you did in times and plus, as in invoke the provided helper methods by adapting the parameters, such as changing sign or swapping them.
-
Unary operators such as change sign:
Fraction negate()
andFraction flip()
that puts it upside down. A more common name for flip would be inverse, however flip landed in the exercise.
And that is 2/3 of the fraction exercise!
As of the moment of writing (Januari 2021, Junit 5.7), the parameterized test can’t deal with varargs parameters, that is a varying list of parameters, with the triple dot notation. |
1.3. Test data from a file
Sometimes the amount of data you have in your test data-set is so big that it does not comfortable fit inside a @CsvSoure
annotation.
You specify an external file as data source with to achieve the same, the annotation now is @CsvFileSource
, which takes files as argument.
The file, as you might have guessed, can be a csv file, which can be edited quite comfortably with a NetBeans plugin or with the help of a spreadsheet program like Libreoffice calc or Microsoft excel.
Suppose you need to develop an input validator that has many test cases. Putting the inputs in a file along with other information relevant to your validator.
@ParameterizedTest
@CsvFileSource( resources = { "testdata.csv" } )
void testRegex( String message, String input, boolean matches, int groupCount ){
// your code here
}
At the time of writing JUnit wants the csv file for CsvFileSource to be |
1.4. Repeated use of same data.
In some cases, multiple tests need the same data. In the case of a CsvSourceFile, that is easily covered: Simple copy the annotations to all places where you need them. This is an acceptable form of copy and waste, because the annotations all point to one source of truth, the CSV file.
Sometimes you would like to keep or even generate the test data inside the test class. Do not take the simple and naive route to simply copy-and-waste the (largish) cvssource data, but instead stick to the D.R.Y. rule.
One problem is that a method in Java can return only one result, either object or primitive type. Luckily an array is also an object in Java.
There are two solutions to this issue.
-
Create special test data objects of a special test data class, either inside your or outside your test class easy to make a test data class to carry the test data. In this case make a data providing method and use the method name in the
@MethodSource
annotation. The test method should understand the test data object. -
Use the JUnit5 provided
ArgumentsProvider
. This warps an array of objects into one object, so that all can be returned as one (array) value in a stream.
This saves the implementation of a (simple) test data class.
We will use the later approach, simply because it is less work on the programmer, either you or me.
1.5. Testing compare results.
You may already know, but do not insist that a comparator (or the compareTo()
method)
should return 1, 0 or -1, because that is not how it is specified.
The value should be greater, equal, or less than zero (0).
To test that, use Integer.signum()
to extract the sign of the actual value and assert that this is equal to the expected value.
@ParameterizedTest
@CsvSource({
"'HELLO', 'world' , -1",// h before w
"'piet' , 'Piet' , 0", // equal ignoring case
"'henk' , 'ab' , 1" // h after a
})
void compareIngnoreCase( String a, String b, int signum ){
assertThat( Integer.signum( a.compareToIgnoreCase​(b) ) ) (1)
.as ( a + " and " + b )
.isEqualTo( signum );
}
1 | compareToXXX call wrapped in Integer.signum(…) . |
1.6. Test Data Using ArgumentsProvider
Using an arguments provider has the advantage of very brief code. You only have to
pay attention that the order of objects in the arrays
that the ArgumentsProvider
provides is the same as the expected order of the parameters to your test method.
In the example below we have put the test data in a two 'dimensional array'.
public class CabbageArgumentsTest {
static final Cabbage SPROUT = new Cabbage( "Brussel Sprouts", 10, 0.2,
STRONG );
static final Cabbage CAULIFLOWER = new Cabbage( "Cauliflower", 1000, 1.5,
DISTINCT );
static final Cabbage KALE = new Cabbage( "Kale", 500, 0.700, WEAK );
static final Cabbage WHITE = new Cabbage( "White Cabbage", 1200, 1.2,
DISTINCT );
static final Cabbage CABBAGE = new Cabbage( "Cabbage", 600, 0.480, STRONG );
static final Cabbage SAVOY = new Cabbage( "Savoy Cabbage", 800, 1.8,
VERY_STRONG );
static Map<String, Comparator<Cabbage>> comparators
= Map.of(
"byStink", ( a, b ) -> a.compareTo( b ),
"byWeight", ( a, b ) -> Double.compare( a.getWeight(), b
.getWeight() ),
"byWeightAlt", Comparator.comparing( Cabbage::getWeight ),
"byWeightNat", Cabbage::compareTo, // 'natural' by weight
"byVolume", Comparator.comparing( Cabbage::getVolume ),
"byWeightNAt2", Comparator.naturalOrder()
);
/**
* Test data table make sure the arguments have the same order as those in
* the test method.
*/
static Object[][] testData
= { // msg, lambda name, result, cab a, cab b
{ "stinks worse", "byStink", 1, SPROUT, CAULIFLOWER },
{ "stinks equal", "byStink", 0, WHITE, CAULIFLOWER },
{ "stinks less", "byStink", -1, CABBAGE, SAVOY }
// other left out
};
/**
* There is only one test, run multiple times, once for each row of test
* data.
*
* @param myData test datum
*/
@Disabled
@ParameterizedTest
@ArgumentsSource( TestData.class )
void testMethod( String message, String comparatorName, int expected,
Cabbage a, Cabbage b ) {
Comparator<Cabbage> comp = comparators.get( comparatorName );
assertThat( Integer.signum( comp.compare( a, b ) ) )
.as( message )
.isEqualTo( expected );
}
/**
* Helper class to pass array data as Arguments Object.
*/
static class TestData implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments( ExtensionContext ec )
throws Exception {
return Arrays.stream( testData ).map( Arguments::of );
}
}
}
*/
public class Cabbage implements Comparable<Cabbage> {
// add the odour enum.
public enum Odour {
// enums are comparable and compare using their natural order.
// the enum later in the list has a higher value in the comparison.
NONE, THRESHOLD, WEAK, DISTINCT, STRONG, VERY_STRONG, INTOLERABLE;
}
final String name;
final int weight;
final double volume;
final Odour odour;
The left out parts are the Constructor and getters for the fields.
1.7. Split it yourselves
Sometimes you need a varying set of values (var-args) in your test, but CsvSource does not accommodate var-args.
In that case you can format one or more of the string such, that you can easily chop it up into an array of Strings, which could then be interpreted as some other type. You only need to choose a separator different than that the CsvSource records use. We have had good experience with the '|' character which is both recognizable but not too obtrusive.
@ParameterizedTest
@CsvSource({
"A, B, C|D|E|F" (1)
})
void someTest(String s1, String s2, String rest) {
String [] restAsArray=rest.split("\\|");
// use in test.
}
1 | You can add spaces in the csvsource lines to improve readability and to align stuff. |
If your varargs parameters provides only single characters, you can split with the empty string into single character String -s.For example "ABC".split("") produces the array {"A", "B", "C"} .Alternatively you can use "ABC".toCharArray() which produces {'A', 'B', 'C'} , an array of primitive char .
|
If your self-split array should contain int values, you can use this helper method.
A similar trick can be applied for long and double too and also for objects that can easily be mapped from a short String.
In the latter case it would be best to also supply a lambda expression to do the conversion, which can then be used in a map(…)
function place of the Integer::parseInt
in the code below.
Such helper can be static an be put in your collection of test helpers.[1]
public static int[] stringToInts( String input ) {
return Stream.of( input.split( "\\|"))
.mapToInt( Integer::parseInt )
.toArray();
}
public static <T> T[] stringToObjects( String input, Function<String, T> fun ) {
return (T[]) Stream.of( input.split( "\\|" ) )
.map( fun )
.toArray( Object[]::new );
}
If you do not yet fully understand what this means, pay attention in week 4.
1.8. Using a method as a Source for the test Data
In all given examples above, the test data is more or less constant, at least the compiler is the last
that somehow computes them. Sometimes you need some kind of processing of the test data. For instance the test data
depend on an environment variable, like the amount of test data, or the test data must be compose from some dynamic value
that is only apparent ate run time. This is where @MethodSource
comes in. At runtime something that can be evaluated can influence the test data.
The use case we had for this the ALDA sorting exercise. We want to run tests even if not all variants are available. We ask the student’s implementation which sorting variants are available, and then prepare a set of test data for each variant.
Technically it works a lot like the ArgumentsProvider. The static
method must return a Stream of some kind. If you choose Arguments
as the stream-element-type,
the test method looks the same as as a normal parameterized test.
public static Stream<Arguments> allSortersAndTestCounts() {
SortKind[] supportedSorters = TestBase.factory.supportedSorters(); (1)
return Stream.of( supportedSorters ).map( ss -> { (2)
return IntStream.of( 2, 3, 10, 100, 1000 )
.mapToObj( i -> arguments( ss, i ) );
} ).flatMap( x -> x ); (3)
}
1 | The number of ready test is determined by the factory, which return am aaar of enum values like QUICK or INSERTION. |
2 | This stream combines each sort kind with a number of elements in a queue to be sorted, as a stream of arguments |
3 | this identity function (lamba) as argument to flatMap unpacks the elements of the inner stream and puts each as element in the outer stream. |
The stream (and lambda) will be further addressed in week 5.
Because we are collecting test tricks, here is another one:
2. Test Recipe I, Test Equals and hashCode
We may sprinkle our testing stuff with a few recipes for often occurring tests. This is the the first installment.
Equals and hashCode are not twins in the direct sense, but indeed methods whose implementation should have a very direct connection. From the java Object API follows that:
-
Two objects that are equal by their equal method, than their hashCode should also be equal.
-
Note that the reverse is not true. If two hashCode are the same, that does not imply that the objects are equal.
-
A good hashCode 'spreads' the objects well, but this is not a very strict requirement or a requirement that can be enforced. A poor hashCode will lead to poor Hash{Map|Set} lookup performance.
Although hashCodes are invented to speedup finding object in a hash map or hash set, these collections use hashCode in the first part of the search, but must verify equality as final step(s).
The thing is that the equals method must consider quite a few things, expressed with conditional evaluation (if-then-else).
The good thing is an IDE typically provides a way to generate equals and hashCode for you and these implementations are typically of good quality. But
in particular the equals method there are quite some ifs, sometimes in disguise, coded as &&
, so this will throw some flies in your code-coverage ointment.
However, we can balance this generated code by a piece of reusable test code, that can be used for almost all cases.
In fact we have not found a case where it does not apply.
Let us first explain the usage and then the implementation.
Suppose you have an object with three fields, name
, birthDate
and id
. All these fields should be considered in equals and hashCode.
As an exercise, create such and object now in your IDE, call it Student, why not.
class Student {
final String name;
final LocalDate birthDate;
final int id;
}
An up to date java programmer would use a Java 16+ record for this kind of entity like classes. The whole definition would then be:
It also saves you the time to both test AND implement equals and hashcode, because that is provided automatically. |
From the above, the IDE can generate a constructor, equals and hashCode and toString. What are you waiting for? Because it is not test driven?
You would be almost right, but why test drive something that can be generated.
However, if your spec does not demand equals and hashCode,
then do not write/generate them. That would be unwanted code. But if the requirements DO insist on equals and hashCode,
make sure that the fields to be considered match the requirements. Choose only final fields.
After having such a generated equals and hashCode you have the predicament of writing a test. HashCode is relatively easy. It should produce an integer, but what value is unspecified, so just invoking it would do the trick for test coverage. The devil is in the equals details, because it has to consider:
-
Is the other object
this
? If yes, returntrue
. -
Is the other object
null
? Returnfalse
if it is. -
Now consider the type.[2].
-
Is the other of the same type as
this
? If not returnfalse
. -
Now we are in known terrain, the other is of the same type, so the object has the same fields.
For each field test itthis.field.equals(other.field)
. If not return false. -
Using
Objects.equals(this.fieldA, other.fieldB)
can be an efficient solution to avoid testing for nullity of either field.
-
@Override
public boolean equals( Object obj ) {
if ( this == obj ) {
return true;
}
if ( obj == null ) {
return false;
}
if ( getClass() != obj.getClass() ) {
return false;
}
final Student other = (Student) obj;
if ( this.id != other.id ) {
return false;
}
if ( !Objects.equals( this.name, other.name ) ) {
return false;
}
return Objects.equals( this.birthDate, other.birthDate );
}
You see a pattern here: The number of ifs is 3 + the number of fields.
To test this, and to make sure you hit all code paths, you need to test with this,
with null, with an distinct (read newly constructed) object with all fields equal,
and then one for each field, which differs from the reference object only in said field.
Define those instances (for this example) as follows.
//@Disabled
@Test
void testEqualsAndHash() {
Student ref = new Student( "Jan", LocalDate.of( 1999, 03, 23 ), 123 );
Student equ = new Student( "Jan", LocalDate.of( 1999, 03, 23 ), 123 );
Student sna = new Student( "Jen", LocalDate.of( 1999, 03, 23 ), 123 ); (1)
Student sda = new Student( "Jan", LocalDate.of( 1998, 03, 23 ), 123 ); (2)
Student sid = new Student( "Jan", LocalDate.of( 1999, 03, 23 ), 456 ); (3)
verifyEqualsAndHashCode( ref, equ, sna, sda, sid );
//fail( "testMethod reached it's and. You will know what to do." );
}
1 | Differs in name. |
2 | Differs in birthdate (year). |
3 | Differs in id. |
The implementation of the verifyEqualsAndHashCode
has been done with generics and a dash of AssertJ stuff.
/**
* Helper for equals tests, which are tedious to get completely covered.
*
* @param <T> type of class to test
* @param ref reference value
* @param equal one that should test equals true
* @param unEqual list of elements that should test unequal in all cases.
*/
public static <T> void verifyEqualsAndHashCode( T ref, T equal, T... unEqual ) {
Object object = "Hello";
T tnull = null;
String cname = ref.getClass().getCanonicalName();
// I got bitten here, assertJ equalTo does not invoke equals on the
// object when ref and 'other' are same.
// THAT's why the first one differs from the rest.
SoftAssertions.assertSoftly( softly-> {
softly.assertThat( ref.equals( ref ) )
.as( cname + ".equals(this): with self should produce true" )
.isTrue();
softly.assertThat( ref.equals( tnull ) )
.as( cname + ".equals(null): ref object "
+ safeToString( ref ) + " and null should produce false"
)
.isFalse();
softly.assertThat( ref.equals( object ) )
.as( cname + ".equals(new Object()): ref object"
+ " compared to other type should produce false"
)
.isFalse();
softly.assertThat( ref.equals( equal ) )
.as( cname + " ref object [" + safeToString( ref )
+ "] and equal object [" + safeToString( equal )
+ "] should report equal"
)
.isTrue();
for ( int i = 0; i < unEqual.length; i++ ) {
T ueq = unEqual[ i ];
softly.assertThat( ref )
.as("testing supposed unequal objects")
.isNotEqualTo( ueq );
}
// ref and equal should have same hashCode
softly.assertThat( ref.hashCode() )
.as( cname + " equal objects "
+ ref.toString() + " and "
+ equal.toString() + " should have same hashcode"
)
.isEqualTo( equal.hashCode() );
});
}
/**
* ToString that deals with any exceptions that are thrown during its
* invocation.
*
* When x.toString triggers an exception, the returned string contains a
* message with this information plus class and system hashCode of the
* object.
*
* @param x to turn into string or a meaningful message.
* @return "null" when x==null, x.toString() when not.
*/
public static String safeToString( Object x ) {
if ( x == null ) {
return "null";
}
try {
return x.toString();
} catch ( Throwable e ) {
return "invoking toString on instance "
+ x.getClass().getCanonicalName() + "@"
+ Integer.toHexString( System.identityHashCode( x ) )
+ " causes an exception " + e.toString();
}
}
The above code has been used before but now adapted for AssertJ and JUnit 5.
It is of course best to put this in some kind of test helper library, so you can reuse it over and over without having to resort to copy and waste.
Exercise Fraction part 3.
Fraction Part 3, Comparable, hashCode and equals
When you thus far did the fraction exercise according to the specifications, it is functionally complete, or as we formally say, the Functional Requirements given are fulfilled. However, the fraction class can be improved a bit, addressing the non functional requirement of user friendliness.
Instead of writing new tests, in some case you can simply add more test data records to your test class for
the tests that probably already exists. It is completely legal to use only part of the the test data.
Or make a special operation in your operation lookup , that produces a fraction with an op out of two
fractions and then 'abuse' the 2nd fraction by taking it’s numerator to get an int that you need in the operation |
-
Creating a fraction in which the denominator is 1 should be written as
new Fraction(x)
,
thereby improving the API. -
Writing a fraction expression is a bit tedious:
Fraction oneThird= new Fraction(1,2).times(new Fraction(2,3))
is a bit of a mouth full.
We want to be able to writeFraction oneThird=frac(1,2).times(frac(2,3))
.
For that we need a static factory method, that takes two ints and creates a new Fraction from them. -
Do the same thing for a one argument
Fraction frac( int )
. -
With fraction you want to be able to see quickly if its value exceeds 1 or not,
so we want the fraction"(3/2)"
to produce a to string of"(1+(1/2))"
.
Overloading, the term used in the text below, means that you have the same method name, but a different parameter list. The difference here is in the difference in the parameter types, not names. |
-
Adding to, subtracting from , multiplying with, or dividing by an integer value should be easier to write.
For instanceFraction two = frac( 1, 4 ).times( 8 )
,
which means the times, divideBy, plus and minus methods all get an overloaded sibling with just one int parameter. As an example of such overloading: for divideBY whe will haveFraction divideBy(Fraction other)
andFraction divideBy(int number)
.
There are also a few functional requirements: we would like to add
-
Equals The fraction does not yet support equals.
Even if numerator and denominator are equal, the inherited fromObject.equals
method will say false when the instances don’t refer to the same fraction object. We need to do better. -
hashCode The fraction has (should have) final fields only, making it a perfect candidate for key in a
HashMap
orHashSet
.
It is also good practice to override hashCode whenever you override equals. -
Comparable Fraction The fraction class should implement the comparable interface, making it easy to test which of two fractions is the biggest.
-
Test equals and HashCode Write a test method to test for
equals
andhashCode
.
Use the utility method mentioned in the previous paragraphs. It is given in the test class.-
Implement hashCode and equals. Use the IDE to generate the methods and accept the result.
-
-
Test Comparable Write a Parameterized test to test the comparable fractions.
A csvsource record could look like"'one third is less that one half', 1|3,1|2, -1"
.
Use at least 3 such records to test all possible outcomes ofcompareTo()
.
Pay attention to the fact that the result of a comparison with compareTo is interpreted wisely, that is, the test record specifies the signum of the result, so assert the signum of the compare-result is equal to the expected value.-
Implement Comparable Make the Fraction comparable by having it implement the corresponding interface.
If the fractions are \(\frac{a}{b}\) and \(\frac{c}{d}\), then it is sufficient to compare the cross product of numerators and denominators \(a\times{d}\) and \(c\times{b}\), because the denominator in the comparison should be made equal, \(b\times{d}\), but needs not to be used. To avoid overflow, use long-math.
This is easily done by usingLong.compare( long, long )
to do the compare work and use the proper expressions as input for the ints.
Cast one of the inputs in the expression to long does the trick. It is called a promotion, in particular a widening conversion.
-
And that makes 3/3 or one whole fraction.
3. Tupples and Record.
Java between 11 and 17 produced quite a few additions to the language. the record type is one of them.
3.1. One return value.
Often enough you need to produce more than just a simple value when returning a result from a method or function. Traditional OO might have solved this by letting the method modify the state of the object on which the method was called. However, immutability of object is fraught with problems which may introduce complexity and by that additional problems. So in the case you need to return more than one value from a method, you were almost forced to define a class with only role to return a tuple. Before java 14 you had to create a complete class, with typically a constructor, getters and toString and in many cases also equals and hascode. That is often a few handfuls of lines for a very simple task.
3.2. immutable or value object as return value.
The record type introduced in java 14 and finalized in java 16 and thus part of the java 17 LTS version solves the above problem quite elegantly.
In many cases when you need a record, you only need to specify type and names of the constituent parts, and the compiler will do the rest without produce code that potentially has to be maintained, which would be the case with IDE-generated code.
As an example, suppose you have an API where you need to return a name and a date. The record definition is as simple as this:
import java.time.LocalDate;
record NameDate( String name, LocalDate date){}
and that is it. What you get is a class with a constructor and whose object provide getters, although named using a different conventions, toString, hashcode and equals, and the objects are immutable as well which is a good thing.
Spelling it out in classic before record classes:
import java.time.LocalDate;
public class NameDateClassic {
private final String name;
private final LocalDate date;
public NameDateClassic(String name, LocalDate date) {
this.name = name;
this.date = date;
}
public String name() { (1)
return name;
}
public LocalDate date() { (2)
return date;
}
@Override
public int hashCode() {
int hash = 7;
hash = 73 * hash + this.name.hashCode();
hash = 73 * hash + this.date.hashCode();
return hash;
}
@Override
public boolean equals(Object obj) {
if ( obj instanceof NameDateClassic other ) { (3)
return this.name.equals( other.name )
&& this.date.equals( other.date );
}
return false;
}
}
1 | and |
2 | use the record convention for getter names. |
3 | Even with the instanceof binding feature since java 14 this classic tuple is quite a handful. |
In the above you can see that a record lets you write in one line what otherwise would have taken some 30 lines.
3.3. Optimal use cases for record.
Use them as 'named' tuples, where the tuple has a name and a type as well as the elements having a name and type each. This is the way, at least in the Java world.
Another use case for record is a Data Transfer Object (DTO), which are used to get data into and out of a database of can be sent through the wire (network). Here record has the added benefit that deserialization is both safe and free, because the constructor is used in that case instead of some obscure constructs.
The use case of DTO is often combined with Data Access Objects (DAO), in particular when you are talking to a database. In that case writing records and the helper class (DAO) can even be less work, because both can easily be generated from the database schema meta information. So generating a DTO+DAO for each table or view can save a lot of typing and testing.
See the coming chapter on reflection and generating code.
Think test driven! So write tests FIRST, and then code. Testing is ensuring that the code is usable. To make code testable is also a design exercise: you need to make decisions about the design of your program when you write your tests. If you cannot test it, you cannot use it. |
Exercise Flawless Password
Flawless passwords
Whenever you register an account somewhere, you are asked to create a password. Usually, this password needs to adhere to a certain standard such as a minimum of eight characters or at least one uppercase letter, a special character such as ampersand (&), star (*), dash (-) or exclamation marks (!).
In this exercise, you have to write methods that validate a password. For example, one method checks whether the password contains an uppercase letter. Remember to start writing the tests first! The password should follow these rules:
-
be at least 10 characters long.
-
contain at least one uppercase letter.
-
contain at least one lowercase letter.
-
contain at least one digit.
-
contain at least one special character, defined as NOT a letter and not a digit.
When all requirements are met, the password will not be blank, so that needs no separate encoding.
The list of special characters in ASCII is !"#$%&'()*,-./:;<=>?@[\]^_{|}~` and +`.
I found that out with the little program `validator.SpecialChars
. It chooses all ASCII characters between character number 33 (space+1).
and 127, excluding those that qualify as letters aor digits.
The java Character class has quite a few static
methods to check if a character classifies as a certain kind of character. For instance
Character.isDigit('9')
will return true
as will Character.isUpper('Ä')
.
The design you are going to implement is using a so called FlawSet, a set of flaws or missing requirements. This uses the approach of elimination. In this case elimination of flaws or missing requirements.
Each Flaw is modeled as a value of the enum
Flaw
which according to the requirements has 5 values.
Each value has a description on what the flaw is, and a character to encode the flaws as a short string. This is helpful in testing as you will see.
In the validator we create a set of flaws and fill it with all possible flaws. If a requirement is met, the flaw (not meeting the requirement) can be removed from the set. If the set is empty, because all potential flaws have been removed, you know that the subject of the flaw tests meets all requirements. It is flawless.
The FlawSet is modeled using an EnumSet. An EnumSet is an implementation of a set with the member type an enum of some sort, the Flaw enum in our case.
An EnumSet is a very space efficient and fast set implementation.
Study Horstmann Vol I Section 9.4.6 and EnumSet api.
The validator test should be set up using a parameterized test, using a CsvSource, in which each line contains a password and the set of flaws that it has. To avoid too long texts in in the table we encode each flaw as a single character, which are hopefully mnemonic, to help readability of the test method. For instance 'U' means missing upper case, 'l' missing lower case. This is the encoding character in the Flaw enum.
The Flaw enum has two convenience methods, which you can use as is.
-
static EnumSet<Flaw> stringToFlawSet( String encoding )
that takes a String and returns a FlawSet that goes with it. -
static Flaw encodingToFlaw( char encoding )
that returns a Flaw value dependent on its encoding character.
import java.util.EnumSet;
import static java.util.EnumSet.noneOf;
import static java.util.stream.Collectors.toCollection;
enum Flaw {
NOUPPER( 'U', "No UPPER case letter" ),
NOLOWER( 'l', "No lower case letter" ),
NODIGIT( '8', "No digit" ),
NOSPECIAL( '#', "No special character" ),
TOO_SHORT( 's', "Too short" );
final char encoding;
final String description;
Flaw( char encoding, String description ) {
this.encoding = encoding;
this.description = description;
}
char getEncoding() {
return encoding;
}
String getDescription() {
return description;
}
static Flaw encodingToFlaw( char encoding ) {
for ( validator.Flaw flaw : values() ) {
if ( flaw.getEncoding() == encoding ) {
return flaw;
}
}
return null;
}
/**
* Collect the encoded flaws into an initially empty set.
* @param encoding the flaws
* @return the set of flaws matching the encoding.
*/
static EnumSet<Flaw> stringToFlawSet( String encoding ) { (1)
return encoding.chars()
.mapToObj( c -> Flaw.encodingToFlaw( (char) c ) ) (2)
.filter( f -> f != null ) (3)
.collect( toCollection( () -> noneOf( Flaw.class ) ) ); (4)
}
}
1 | Do not worry if you do not understand this immediately, the details will follow starting week 4 or 5. |
2 | Hey, still reading? You seem interested. This converts the int stream into an object stream of enum values. |
3 | We do not want null values. Might happen when an encoding is not valid. |
4 | Then collect it into a collection, in the specific case an EnumSet of Flaw values. We start with the empty set, which will
be filled with the values resulting from the stream.Note that we left out the imports. |
The Flaw
enum and it’s test are already in the project. I can’t hurt to study the tests.
encoding | Flaw |
---|---|
'U' |
No UPPER case letter |
'l' |
No lower case letter |
'8' |
No digit |
'#' |
No special character |
's' |
Too short |
Write two tests,
-
invalidPassword that checks for invalid passwords as
@ParameterizedTest
which uses a table of invalid password and the encoding of the flaws to detect. -
validPassword that accepts (one) or two valid passwords. You can also implement that as a
@ParameterizedTest
, but in the one password variant, you can get by with a simple unit test method.
First write the test for invalid passwords. Use a @ParameterizedTest
and a @CvsSource
.
The test should assert that an exception is thrown and that all desciptions of the found flaws are present in the message.
Make sure you have a csv test line for each of the failure cases, but also test for passwords that have multiple flaws.
Best is to write all the invalid password and their encodings, but comment out the ones you are not yet testing.
The csv records should contain two values, a flawed password and the encoded flaws.
Example "'password',U8#s"
because the password does not contain digits, UPPER case, nor special characters, and is too short, the encoded flaws are U8#s
.
You can apply the trick to split with an empty string to get the encodings as 1 character string. Or you can stream the encoding with String.
Note that the order of the encoding characters should not matter, but proper coding style might say write them consistently in definition- or alphabetical order.
Choice is up to you. We chose the former.
When the password is invalid, throw an IllegalPasswordException
with a message that contains the descriptions of all flaws found.
The idea of the validator method is that you initially fill an EnumSet with all possible flaw values, which is very easy: EnumSet.allOf( Flaw.class )
.
Then for every character of the password check,
check if it fulfills a requirement. If it does, you can eliminate the corresponding Flaw from the set.
See the EnumSet
or Set API on how to do that.
Once you have used up all the characters in the password, check if the set is empty.
If it is, the password meets it’s requirement and is flawless;
if not, the set contains the requirements not fulfilled and the password is to be considered flawed.
Exercise Enum Calculator
Exercise 3: Enum Calculator
In this exercise you build a calculator that has its operations expressed in an enum called Operator
.
This enum uses a field of type IntBinaryOperator
which itself is a Functional interface, specifies the method applyAsInt(int a, int b)
.
You will use that field to evaluate the expression.
The enum values in Operation and ADD, SUBTRACT,
MULTIPLY, DIVIDE, and POWER with the usual meaning.
Each enum value has a symbol of type String
"+"
for ADD, etc and "**"
for the Power operation.
Your task is to test driven develop the Operator-enum. The main class called Calculator is given. It uses the Operator to do the actual computation.
It can compute simple space-separated integer expressions like 2 + 3
of 2 ** 10
.
The functional requirements are:
-
The Operator class has a constructor that takes 2 parameters:
-
a symbol string and
-
a IntBinaryOperator functional interface, which defines the shape of a lambda expression.
You use these to supply the actual computation to the enum value like inADD( "+", ( a, b ) → a + b )
for value ADD, symbol + and lambda( a, b ) → a + b )
. Both constructor parameters are saved as fields.
-
-
You should be able to lookup the Operator by its symbol, using a static method
Operator get(String symbol)
. -
The Operator has a compute function
int compute(int a, int b)
, which does the actual work and uses the method applyAsInt. -
In most cased the lambda expression fits on the same line as the symbol. In case of the
**
, use a loop or stream to do the exponentiation.
Op | Symbol |
---|---|
ADD |
'+' |
SUBTRACT |
'-' |
MULTIPLY |
'*' |
DIVIDE |
'/' |
POWER |
'**' |
The main class, Calculator reads the input as lines and splits the input by white space, to make parsing easier. So you will have to give the input values and operator separated by white space. See below.
The non-functional requirements regarding the tests are that you use a JUnit 5 @ParameterizedTest
.
@ParameterizedTest
@CsvSource( {
// the compiler will turn the lines in one string each.
// eg the first line results in the String "add,+,6, 2, 3 "
// we use this technique to let the compiler
// do the _hard_ work of computing the result values.
// message, sym, expected, a,b
"add, +," + ( 2 + 3 ) + ", 2, 3 ",
"subtract, -," + ( 2 - 3 ) + ", 2, 3 ",
"multiply, *," + ( 2 * 3 ) + ", 2, 3 ",
"divide, /," + ( 2 / 3 ) + ", 2, 3 ",
"power, **," + ( 2 * 2 * 2 ) + ", 2, 3 ",
} )
public void testOperator( String message, String symbol,
int expected, int a, int b ) {
// test code left out, the exercise ;-))
}
The app that uses the Operator class is shown below.
class Calculator {
public static void main( String[] args ) {
Scanner scanner = new Scanner( System.in );
System.out.print( "calc: " );
String line = scanner.nextLine();
String[] tokens = line.split( "\\s+" );
while ( tokens.length >= 3 ) {
System.out.println( line + " = " + compute( tokens ) );
System.out.print( "calc: " );
line = scanner.nextLine();
tokens = line.split( "\\s+" );
}
}
/**
* Compute takes three tokens, a, operatorSymbol, and b as in "5", "+"," "6".
* @param tokens to be evaluated.
*/
static int compute( String... tokens ) {
if ( tokens.length < 3 ) {
return 0;
}
int a = Integer.parseInt( tokens[ 0 ] );
int b = Integer.parseInt( tokens[ 2 ] );
String symbol = tokens[ 1 ];
return Operator.get( symbol ).compute( a, b ); (1)
}
}
1 | lookup operation and apply it to the values. |
calc: 3 ** 8
3 ** 8 = 6561
calc: 4 + 12
4 + 12 = 16