This proposal is documented in java
but the code is available in other languages (C#
, kotlin
, scala
, F#
).
Based on the specifications, we already have a test list to start with :
Invalid NIRs
- 2230 // too short
- 323115935012322 // incorrect sex
- 2ab115935012322 // incorrect year
- 223ab5935012322 // incorrect month
- 223145935012322 // incorrect month 2
- 223005935012322 // incorrect month 3
- 22311xx35012322 // incorrect department
- 223119635012322 // incorrect department 2
- 2231159zzz12322 // incorrect city
- 223115935012321 // incorrect control key
- 2231159350123221 // too long
Valid NIRs
- 223115935012322
- 200029923123486
- 254031088723464
- 195017262676215
- 155053933981739
- 106099955391094
π΄ Let's start by a failing test (as usual in T.D.D).
What happens if the client code sends an empty String?
class ValidateNIR {
@Test
void validate_empty_string_should_return_false() {
assertThat(NIR.validate(""))
.isFalse();
}
}
π’ Make it green
as fast as possible by generating production code from usage.
public class NIR {
public static Boolean validate(String potentialNIR) {
return false;
}
}
π΅ Refactor the code to introduce an empty String
rule.
public class NIR {
public static Boolean validate(String potentialNIR) {
return potentialNIR != "";
}
}
π΄ Add a second test case for too short String
.
@Test
void validate_short_string_should_return_false() {
assertThat(NIR.validate("2230"))
.isFalse();
}
π’ Add a length
rule.
public class NIR {
public static Boolean validate(String potentialNIR) {
return potentialNIR != "" && potentialNIR.length() == 15;
}
}
π΅ Simplify the expression and remove magic number
.
public class NIR {
private static final int VALID_LENGTH = 15;
public static Boolean validate(String potentialNIR) {
return potentialNIR.length() == VALID_LENGTH;
}
}
Our test list is now looking like this:
Invalid NIRs
β
empty string
β
2230 // too short
- 323115935012322 // incorrect sex
- 2ab115935012322 // incorrect year
- 223ab5935012322 // incorrect month
- 223145935012322 // incorrect month 2
- 223005935012322 // incorrect month 3
- 22311xx35012322 // incorrect department
- 223119635012322 // incorrect department 2
- 2231159zzz12322 // incorrect city
- 223115935012321 // incorrect control key
- 2231159350123221 // too long
Valid NIRs
- 223115935012322
- 200029923123486
- 254031088723464
- 195017262676215
- 155053933981739
- 106099955391094
π΄ Let's add a new expectation from our tests.
@Test
void validate_with_invalid_sex_should_return_false() {
assertThat(NIR.validate("323115935012322"))
.isFalse();
}
π’ Express sex validation.
public static Boolean validate(String potentialNIR) {
return potentialNIR.length() == VALID_LENGTH
&& (potentialNIR.charAt(0) == '1' || potentialNIR.charAt(0) == '2');
}
π΅ Extract named method for better comprehension.
public class NIR {
private static final int VALID_LENGTH = 15;
public static Boolean validate(String potentialNIR) {
return validateLength(potentialNIR)
&& validateSex(potentialNIR);
}
private static boolean validateLength(String potentialNIR) {
return potentialNIR.length() == VALID_LENGTH;
}
private static boolean validateSex(String potentialNIR) {
return potentialNIR.charAt(0) == '1' || potentialNIR.charAt(0) == '2';
}
}
In refactoring stage, you should always wonder how to improve test code as well. We have already some duplication in our tests. We could use parameterized tests instead of maintaining 1 test method per "test case".
With junit
, we can create Parameterized Tests
by using junit-jupiter-params
.
class ValidateNIR {
public static Stream<Arguments> invalidNIRs() {
return Stream.of(
Arguments.of("", "empty string"),
Arguments.of("2230", "too short"),
Arguments.of("323115935012322", "incorrect sex")
);
}
@ParameterizedTest
@MethodSource("invalidNIRs")
void should_return_false(String input, String reason) {
assertThat(NIR.validate(input))
.isFalse()
.as(reason);
}
}
Test output is now looking like this:
π΄ Continue to add feature from our test list.
public static Stream<Arguments> invalidNIRs() {
return Stream.of(
...
Arguments.of("2ab115935012322", "incorrect year")
);
}
Be careful when using
Parameterized tests
to move 1 test case at a time.
π’ Make it pass by hardcoding year validation.
public class NIR {
private static final int VALID_LENGTH = 15;
public static Boolean validate(String potentialNIR) {
return validateLength(potentialNIR)
&& validateSex(potentialNIR)
&& validateYear(potentialNIR);
}
private static boolean validateLength(String potentialNIR) {
return potentialNIR.length() == VALID_LENGTH;
}
private static boolean validateSex(String potentialNIR) {
return potentialNIR.charAt(0) == '1' || potentialNIR.charAt(0) == '2';
}
private static boolean validateYear(String potentialNIR) {
return false;
}
}
π΅ Implement year validation as expressed in specification.
I choose to use Regex to check whether Year is a valid number or not
.
private static boolean validateYear(String potentialNIR) {
return potentialNIR
.substring(1, 3)
.matches("[0-9.]+");
}
The whole class can still be improved -> pass only the needed characters for inner validation.
public class NIR {
private static final int VALID_LENGTH = 15;
private static final char MALE = '1', FEMALE = '2';
public static Boolean validate(String potentialNIR) {
return validateLength(potentialNIR)
&& validateSex(potentialNIR.charAt(0))
&& validateYear(potentialNIR.substring(1, 3));
}
private static boolean validateLength(String potentialNIR) {
return potentialNIR.length() == VALID_LENGTH;
}
private static boolean validateSex(char sex) {
return sex == MALE || sex == FEMALE;
}
private static boolean validateYear(String year) {
return isANumber(year);
}
private static boolean isANumber(String potentialNumber) {
return potentialNumber.matches("[0-9.]+");
}
}
Invalid NIRs
β
empty string
β
2230 // too short
β
323115935012322 // incorrect sex
β
2ab115935012322 // incorrect year
- 223ab5935012322 // incorrect month
- 223145935012322 // incorrect month 2
- 223005935012322 // incorrect month 3
- 22311xx35012322 // incorrect department
- 223119635012322 // incorrect department 2
- 2231159zzz12322 // incorrect city
- 223115935012321 // incorrect control key
- 2231159350123221 // too long
Valid NIRs
- 223115935012322
- 200029923123486
- 254031088723464
- 195017262676215
- 155053933981739
- 106099955391094
Here are the iterations (You can see their details from the git history):
Here is where we are in our test list:
Invalid NIRs
β
empty string
β
2230 // too short
β
323115935012322 // incorrect sex
β
2ab115935012322 // incorrect year
β
223ab5935012322 // incorrect month
β
223145935012322 // incorrect month 2
β
223005935012322 // incorrect month 3
β
22311xx35012322 // incorrect department
β
223119635012322 // incorrect department 2
β
2231159zzz12322 // incorrect city
β
223115935012321 // incorrect control key
β
2231159350123221 // too long
Valid NIRs
- 223115935012322
- 200029923123486
- 254031088723464
- 195017262676215
- 155053933981739
- 106099955391094
Some interesting stuff made during the implementation.
- Use
lombok
to defineExtension methods
onString
- Use
vavr
to use a more functional and less imperative way of coding
@UtilityClass
public class StringExtensions {
// Extension methods are static methods with at least 1 parameter
// The first parameter type is the one we extend
public static Option<Integer> toInt(String potentialNumber) {
return isANumber(potentialNumber) // Use Option<Integer> -> equivalent to Optional since java 8
? some(Integer.parseInt(potentialNumber))
: none();
}
private static boolean isANumber(String str) {
return str != null && str.matches("[0-9.]+");
}
}
- Use
String
extension methods from our production code
@UtilityClass
// Reference extension classes
@ExtensionMethod(StringExtensions.class)
public class NIR {
...
private static boolean validateMonth(String month) {
return validateNumber(month, x -> x > 0 && x <= 12);
}
private static boolean validateDepartment(String department) {
return validateNumber(department, x -> x > 0 && (x <= 95 || x == 99));
}
// A generic method that parses a String then apply a validation function on it to check whether the value ensures it
// Here is its signature: String -> (int -> bool) -> bool
private static boolean validateNumber(String potentialNumber, Function<Integer, Boolean> isValid) {
return potentialNumber
.toInt() // return an Option<Integer> (Some if something or None)
.map(isValid) // called only if Some
.getOrElse(false); // if none returns false
}
private static boolean isANumber(String potentialNumber) {
return potentialNumber.matches("[0-9.]+");
}
}
Because we have used Triangulation
on invalid NIRs we already have created a general implementation.
The more specific tests you write, the more the code will become generic.
All our valid NIRs
are already well validated, we do not have to modify anything in our production code.
class ValidateNIR {
public static Stream<Arguments> invalidNIRs() {
return Stream.of(
Arguments.of("", "empty string"),
Arguments.of("2230", "too short"),
Arguments.of("323115935012322", "incorrect sex"),
Arguments.of("2ab115935012322", "incorrect year"),
Arguments.of("223ab5935012322", "incorrect month"),
Arguments.of("223145935012322", "incorrect month 2"),
Arguments.of("223005935012322", "incorrect month 3"),
Arguments.of("22311xx35012322", "incorrect department"),
Arguments.of("223119635012322", "incorrect department 2"),
Arguments.of("2231159zzz12322", "incorrect city"),
Arguments.of("223115935012321", "incorrect key"),
Arguments.of("2231159350123221", "too long")
);
}
public static Stream<Arguments> validNIRs() {
return Stream.of(
Arguments.of("223115935012322"),
Arguments.of("200029923123486"),
Arguments.of("254031088723464"),
Arguments.of("195017262676215"),
Arguments.of("155053933981739"),
Arguments.of("106099955391094")
);
}
@ParameterizedTest
@MethodSource("invalidNIRs")
void should_return_false(String input, String reason) {
assertThat(NIR.validate(input))
.as(reason)
.isFalse();
}
@ParameterizedTest
@MethodSource("validNIRs")
void should_return_true(String input) {
assertThat(NIR.validate(input))
.isTrue();
}
}
Here is our final test list state from this stage.
Invalid NIRs
β
empty string
β
2230 // too short
β
323115935012322 // incorrect sex
β
2ab115935012322 // incorrect year
β
223ab5935012322 // incorrect month
β
223145935012322 // incorrect month 2
β
223005935012322 // incorrect month 3
β
22311xx35012322 // incorrect department
β
223119635012322 // incorrect department 2
β
2231159zzz12322 // incorrect city
β
223115935012321 // incorrect control key
β
2231159350123221 // too long
Valid NIRs
β
223115935012322
β
200029923123486
β
254031088723464
β
195017262676215
β
155053933981739
β
106099955391094
Know more about Primitive Obsession
here
Let's apply "Parse Don't Validate" principle to fight "Primitive Obsession".
We will use Property Based Testing
in this part of the kata to design our parser.
Our parsing function
must respect the below property
for all (validNir)
parseNIR(nir.toString) == nir
With parse don't validate
we want to make it impossible to represent an invalid NIR
in our system. Our data structures need to be immutables
.
Our parser may look like this: String -> Either<ParsingError, NIR>
- Add
vavr-test
to do so
testImplementation("io.vavr:vavr-test:0.10.4")
π΄ Specify the property
class NIRProperties {
private Arbitrary<NIR> validNIR = null;
@Test
void roundTrip() {
Property.def("parseNIR(nir.ToString()) == nir")
.forAll(validNIR)
.suchThat(nir -> NIR.parse(nir.toString()).contains(nir))
.check()
.assertIsSatisfied();
}
}
π’ Make it pass.
- Generate the
NIR
class - Handle error with a data structure:
ParsingError
public record ParseError(String message) {
}
@EqualsAndHashCode
public class NIR {
public static Either<ParsingError, NIR> parse(String input) {
return right(new NIR());
}
@Override
public String toString() {
return "";
}
}
class NIRProperties {
private Arbitrary<NIR> validNIR = Arbitrary.of(new NIR());
@Test
void roundTrip() {
Property.def("parseNIR(nir.ToString()) == nir") // describe the property
.forAll(validNIR) // pass an Arbitrary / Generator to generate valid NIRs
.suchThat(nir -> NIR.parse(nir.toString()).contains(nir)) // describe the Property predicate
.check()
.assertIsSatisfied();
}
}
π΅ Create the Sex
type
- We choose to use an
enum
for that - It is immutable by design
- We need to work on the
String
representation of it - Each data structure will contain its own parsing method
public enum Sex {
M(1), F(2);
private final int value;
Sex(int value) {
this.value = value;
}
public static Either<ParsingError, Sex> parseSex(char input) {
// vavr Pattern matching
return Match(input).of(
Case($('1'), right(M)),
Case($('2'), right(F)),
Case($(), left((new ParsingError("Not a valid sex"))))
);
}
@Override
public String toString() {
return "" + value;
}
}
- Create a generator to be able to generate valid NIRs
private final Gen<Sex> sexGenerator = Gen.choose(Sex.values());
- Extend
NIR
with the new created type
@EqualsAndHashCode
public class NIR {
private final Sex sex;
public NIR(Sex sex) {
this.sex = sex;
}
public static Either<ParsingError, NIR> parseNIR(String input) {
return parseSex(input.charAt(0))
.map(NIR::new);
}
@Override
public String toString() {
return sex.toString();
}
}
class NIRProperties {
private final Gen<Sex> sexGenerator = Gen.choose(Sex.values());
private final Arbitrary<NIR> validNIR =
sexGenerator.map(NIR::new)
.arbitrary();
@Test
void roundTrip() {
Property.def("parseNIR(nir.ToString()) == nir")
.forAll(validNIR)
.suchThat(nir -> NIR.parseNIR(nir.toString()).contains(nir))
.check()
.assertIsSatisfied();
}
}
Like for the Sex
type, we design the new type with its generator.
π΄ create a generator
private final Gen<Year> yearGenerator = Gen.choose(0, 99).map(Year::fromInt); // have a private constructor
private final Gen<Sex> sexGenerator = Gen.choose(Sex.values());
private final Arbitrary<NIR> validNIR =
sexGenerator
.map(NIR::new)
// use the yearGenerator here
.arbitrary();
π’ To be able to use the yearGenerator
, we need to have a context to be able to map into it.
It is a mutable data structure that we enrich with the result of each generator. We create a Builder
class for it:
@With
@Getter
@AllArgsConstructor
public class NIRBuilder {
private final Sex sex;
private Year year;
public NIRBuilder(Sex sex) {
this.sex = sex;
}
}
- We now adapt the
NIRProperties
to use thisBuilder
class NIRProperties {
private final Random random = new Random();
private final Gen<Year> yearGenerator = Gen.choose(0, 99).map(Year::fromInt);
private final Gen<Sex> sexGenerator = Gen.choose(Sex.values());
private Arbitrary<NIR> validNIR =
sexGenerator
.map(NIRBuilder::new)
.map(builder -> builder.withYear(yearGenerator.apply(random)))
.map(x -> new NIR(x.getSex(), x.getYear()))
.arbitrary();
@Test
void roundTrip() {
Property.def("parseNIR(nir.ToString()) == nir")
.forAll(validNIR)
.suchThat(nir -> NIR.parseNIR(nir.toString()).contains(nir))
.check()
.assertIsSatisfied();
}
}
- We now have to adapt the
NIR
class to handle theYear
in its construct- We will use the same
Builder
construct (in other languages we may usefor comprehension
orLinQ
for example)
- We will use the same
@EqualsAndHashCode
@AllArgsConstructor
public class NIR {
private final Sex sex;
private final Year year;
public static Either<ParsingError, NIR> parseNIR(String input) {
return parseSex(input.charAt(0))
.map(NIRBuilder::new)
.flatMap(builder -> right(builder.withYear(new Year(1))))
.map(builder -> new NIR(builder.getSex(), builder.getYear()));
}
@Override
public String toString() {
return sex.toString() + year;
}
}
π΅ We can now work on the Year
type and its parser
@EqualsAndHashCode
@AllArgsConstructor
public class NIR {
private final Sex sex;
private final Year year;
public static Either<ParsingError, NIR> parseNIR(String input) {
return parseSex(input.charAt(0))
.map(NIRBuilder::new)
.flatMap(builder -> parseYear(input.substring(1, 3), builder))
.map(builder -> new NIR(builder.getSex(), builder.getYear()));
}
private static Either<ParsingError, NIRBuilder> parseYear(String input, NIRBuilder builder) {
return Year.parseYear(input)
.map(builder::withYear);
}
@Override
public String toString() {
return sex.toString() + year;
}
}
@EqualsAndHashCode
@ExtensionMethod(StringExtensions.class)
public class Year {
private final int value;
public Year(int value) {
this.value = value;
}
public static Either<ParsingError, Year> parseYear(String input) {
return input.toInt()
.map(Year::new)
.toEither(new ParsingError("year should be between 0 and 99"));
}
public static Year fromInt(Integer x) {
return parseYear(x.toString())
.getOrElseThrow(() -> new IllegalArgumentException("Year"));
}
@Override
public String toString() {
return String.format("%02d", value);
}
}
We can check Properties
generation by printing the generated nirs:
214
241
240
182
138
294
280
252
158
265
213
225
275
Here are the iterations (You can see their details from the git history):
For now:
- It is impossible to represent a
NIR
in an invalid state - We have a semantic that expresses the concepts behind
NIR
@Override
public String toString() {
return stringWithoutKey() + format("%02d", key());
}
// How the NIR is composed
private String stringWithoutKey() {
return sex.toString() + year + month + department + city + serialNumber;
}
Is it enough by designing our types like this?
Let's create a property that demonstrates that an invalid NIR
can never be parsed.
We will generate a valid one and then mutate its string representation to create an invalid one.
For that we will create some mutators.
for all (validNir)
mutate(nir.toString) == left
private record Mutator(String name, Function1<NIR, Gen<String>> mutate){}
private static Mutator sexMutator = new Mutator("Sex mutator", nir ->
Gen.choose(3, 9)
.map(invalidSex -> invalidSex + nir.toString().substring(1))
);
- Define the
property
and use the mutator
class NIRMutatedProperties {
private static final Random random = new Random();
private record Mutator(String name, Function1<NIR, Gen<String>> func) {
public String mutate(NIR nir) {
return func.apply(nir).apply(random);
}
}
private static Mutator sexMutator = new Mutator("Sex mutator", nir ->
Gen.choose(3, 9)
.map(invalidSex -> invalidSex + nir.toString().substring(1))
);
private static Arbitrary<Mutator> mutators = Gen.choose(
sexMutator).arbitrary();
@Test
void invalidNIRCanNeverBeParsed() {
Property.def("parseNIR(nir.ToString()) == nir")
.forAll(validNIR, mutators)
.suchThat(NIRMutatedProperties::canNotParseMutatedNIR)
.check()
.assertIsSatisfied();
}
private static boolean canNotParseMutatedNIR(NIR nir, Mutator mutator) {
return NIR.parseNIR(mutator.mutate(nir)).isLeft();
}
}
- Define mutators like this:
β
Sex Mutator
Truncate the NIR
Year Mutator
Month Mutator
Department Mutator
City Mutator
Serial Number Mutator
Key Mutator
- Truncate a valid
NIR
string
private static Mutator truncateMutator = new Mutator("Truncate mutator", nir ->
Gen.choose(1, 13).map(size ->
size == 1 ? "" : nir.toString().substring(0, size - 1)
)
);
- Let's use it in our
property
by simply adding it to the mutators
private static Arbitrary<Mutator> mutators = Gen.choose(
sexMutator,
truncateMutator
).arbitrary();
- By using it, we can identify that we have an issue regarding parsing a
String
that has not the right size- We can now fix our production code
@EqualsAndHashCode
@AllArgsConstructor
@ExtensionMethod(StringExtensions.class)
public class NIR {
...
public static Either<ParsingError, NIR> parseNIR(String input) {
// Handle the length of the input
return input.length() != VALID_LENGTH
? left(new ParsingError("Not a valid NIR: should have a length of " + input.length()))
: parseSafely(input);
}
private static Either<ParsingError, NIR> parseSafely(String input) {
return toNIR(input)
.flatMap(nir -> checkKey(input.substring(13), nir));
}
private static Either<ParsingError, NIR> toNIR(String input) {
return parseSex(input.charAt(0))
.map(NIRBuilder::new)
.flatMap(builder -> parseYear(input.substring(1, 3), builder))
.flatMap(builder -> parseMonth(input.substring(3, 5), builder))
.flatMap(builder -> parseDepartment(input.substring(5, 7), builder))
.flatMap(builder -> parseCity(input.substring(7, 10), builder))
.flatMap(builder -> parseSerialNumber(input.substring(10, 13), builder))
.flatMap(builder -> parseKey(input.substring(13, 15), builder))
.map(builder ->
new NIR(
builder.getSex(),
builder.getYear(),
builder.getMonth(),
builder.getDepartment(),
builder.getCity(),
builder.getSerialNumber()
)
);
}
...
}
β
Sex Mutator
β
Truncate the NIR
Year Mutator
Month Mutator
Department Mutator
City Mutator
Serial Number Mutator
Key Mutator
Here are the iterations (you can see their details from the git history):
β
Sex Mutator
β
Truncate the NIR
β
Year Mutator
β
Month Mutator
β
Department Mutator
β
City Mutator
β
Serial Number Mutator
β
Key Mutator
At the end, the code looks like this:
public class Mutators {
private static Gen<Integer> digits3Gen = Gen.frequency(
Tuple.of(7, Gen.choose(1000, 9999)),
Tuple.of(3, Gen.choose(1, 99))
);
private static Mutator sexMutator = new Mutator("Sex mutator", nir ->
Gen.choose(3, 9).map(invalidSex -> concat(invalidSex, nir.toString().substring(1)))
);
private static Mutator yearMutator = new Mutator("Year mutator", nir ->
Gen.frequency(
Tuple.of(7, Gen.choose(100, 999)),
Tuple.of(3, Gen.choose(1, 9))
).map(invalidYear -> concat(
nir.toString().charAt(0),
invalidYear,
nir.toString().substring(3)
)
)
);
private static Mutator departmentMutator = new Mutator("Department mutator", nir ->
Gen.frequency(
Tuple.of(7, Gen.choose(100, 999)),
Tuple.of(3, Gen.choose(96, 98))
).map(invalidDepartment -> concat(
nir.toString().substring(0, 5),
invalidDepartment,
nir.toString().substring(7)
)
)
);
private static Mutator cityMutator = new Mutator("City mutator", nir ->
digits3Gen.map(invalidCity -> concat(
nir.toString().substring(0, 7),
invalidCity,
nir.toString().substring(10))
)
);
private static Mutator serialNumberMutator = new Mutator("Serial Number mutator", nir ->
digits3Gen.map(invalidSerialNumber -> concat(
nir.toString().substring(0, 10),
invalidSerialNumber,
nir.toString().substring(13))
)
);
private static Mutator keyMutator = new Mutator("Key mutator", nir ->
Gen.choose(0, 97)
.filter(x -> x != nir.key())
.map(invalidKey -> concat(
nir.toString().substring(0, 13),
String.format("%02d", invalidKey)
))
);
private static String concat(Object... elements) {
return List.of(elements).mkString();
}
private static Mutator truncateMutator = new Mutator("Truncate mutator", nir ->
Gen.choose(1, 13).map(size ->
size == 1 ? "" : nir.toString().substring(0, size - 1)
)
);
public static Arbitrary<Mutator> mutators = Gen.choose(
sexMutator,
yearMutator,
departmentMutator,
cityMutator,
serialNumberMutator,
keyMutator,
truncateMutator
).arbitrary();
}
class MutatedProperties {
@Test
void invalidNIRCanNeverBeParsed() {
Property.def("parseNIR(nir.ToString()) == nir")
.forAll(validNIR, mutators)
.suchThat(MutatedProperties::canNotParseMutatedNIR)
.check()
.assertIsSatisfied();
}
private static boolean canNotParseMutatedNIR(NIR nir, Mutator mutator) {
return NIR.parseNIR(mutator.mutate(nir)).isLeft();
}
}
By using this approach you can mix T.D.D
with Type Driven Development
, Property-Based Testing
and Mutations
to design extremely robust code.
Indeed, it can help you quickly identify edge cases in your system.