From 6902bf3d389b29348404bbcb7b93f4cb886b29f9 Mon Sep 17 00:00:00 2001 From: Bonajo Date: Tue, 27 Feb 2024 08:29:08 +0000 Subject: [PATCH] deploy: 1d86db2bd6816bd207ea31aa4f1a70b4bf801179 --- docs/index.xml | 2 +- docs/parameterized-tests/index.html | 208 ++++++++++++++++++++++++++++ docs/unit-testing-basics/index.html | 3 +- index.html | 3 +- index.xml | 2 +- pages/setup/index.html | 3 +- pages/tips/index.html | 3 +- sitemap.xml | 2 +- 8 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 docs/parameterized-tests/index.html diff --git a/docs/index.xml b/docs/index.xml index 63243ab..fc1b995 100644 --- a/docs/index.xml +++ b/docs/index.xml @@ -1 +1 @@ -Docs on PRC2https://fontysvenlo.github.io/prc2/docs/Recent content in Docs on PRC2Hugo -- gohugo.ioen-usTue, 01 Feb 2022 11:32:04 +010001 Unit Testing Basicshttps://fontysvenlo.github.io/prc2/docs/unit-testing-basics/Tue, 01 Feb 2022 11:32:04 +0100https://fontysvenlo.github.io/prc2/docs/unit-testing-basics/Table of ContentsWrite your own tests!Testing / Test Driven DevelopmentWhat are tests and why do we need them?Test Driven Development (TDD)Arrange Act AssertClues neededAssertJ examples.Simple TestsString ContainmentCollection ContainmentAssert ExceptionsSoft AssertionsAssumptionsAdditional pointersWrite your own tests!Throughout the exercises of PRC1, you have become acquainted with the value of tests: You have a way of checking if your code is any good without having to test each and every part manually. You clicked on the nice TMC button in NetBeans and then you could see which parts of your code worked, and which didn’t. \ No newline at end of file +Docs on PRC2https://fontysvenlo.github.io/prc2/docs/Recent content in Docs on PRC2Hugo -- gohugo.ioen-usTue, 01 Feb 2022 14:17:33 +010001 Unit Testing Basicshttps://fontysvenlo.github.io/prc2/docs/unit-testing-basics/Tue, 01 Feb 2022 11:32:04 +0100https://fontysvenlo.github.io/prc2/docs/unit-testing-basics/Table of ContentsWrite your own tests!Testing / Test Driven DevelopmentWhat are tests and why do we need them?Test Driven Development (TDD)Arrange Act AssertClues neededAssertJ examples.Simple TestsString ContainmentCollection ContainmentAssert ExceptionsSoft AssertionsAssumptionsAdditional pointersWrite your own tests!Throughout the exercises of PRC1, you have become acquainted with the value of tests: You have a way of checking if your code is any good without having to test each and every part manually. You clicked on the nice TMC button in NetBeans and then you could see which parts of your code worked, and which didn’t.02 Parameterized testshttps://fontysvenlo.github.io/prc2/docs/parameterized-tests/Tue, 01 Feb 2022 14:17:33 +0100https://fontysvenlo.github.io/prc2/docs/parameterized-tests/Table of ContentsParameterized testsParameterized test, Junit 5 styleLookup in a map.Test data from a fileRepeated use of same data.Test Recipe I, Test Equals and hashCodeLinksParameterized testsYou 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. \ No newline at end of file diff --git a/docs/parameterized-tests/index.html b/docs/parameterized-tests/index.html new file mode 100644 index 0000000..5de3ef4 --- /dev/null +++ b/docs/parameterized-tests/index.html @@ -0,0 +1,208 @@ +PRC2 - 02 Parameterized tests +
+PRC2

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.

Warning:

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.

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.

Complete fraction test class with parameterized test, using an in-code csv source
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.


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).

map from short string to test (Person) object.
 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. +You can 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.

Map of string to lambda
    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> existing functional interface, +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.
Using lambda names in test data
    @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.
Test method using the test data (Annotations left out, they are above).
    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 '2021-01-14' for the day of writing this). Strings in the csv-records will be automatically converted to the parameter types in your test methods in these cases.
  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 that is minding upper and lower case usage.

Study the examples above, they might give you inspiration with the exercises coming up and will score you points during the exam.

Test data from a file

Sometimes the amount of data you have in your test data-set is so big that it does not comfortably 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.

csvfile source
  @ParameterizedTest
+  @CsvFileSource( resources = { "testdata.csv" } )
+  void testRegex( String message, String input, boolean matches, int groupCount ){
+    // your code here
+  }

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.

  1. 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.
  2. Use the JUnit5 provided ArgumentsProvider. This wraps 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.

Have a look at the JUnit5 documentation to find a further explanation and examples.


Because we are collecting test tricks, here is another one:

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 in 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;
+}

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, return true.
  • Is the other object null? Return false if it is.
  • Now consider the type.[1].
    • Is the other of the same type as this? If not return false.
    • Now we are in known terrain, the other is of the same type, so the object should have the same fields.
      For each field test it this.field.equals(other.field). If not return false.
    • Using Objects.equals(this.fieldA, other.fieldA) can be an efficient solution to avoid testing for nullity of either field.
Generated equals. It is fine.
    @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.

Instances for complete equals and hashCode test and coverage
    //@Disabled
+    @Test
+    void testEqualsAndHash() {
+        Student ref = new Student( "Jan", LocalDate.of( 1999, 03, 23 ), 123 ); 1
+        Student equ = new Student( "Jan", LocalDate.of( 1999, 03, 23 ), 123 ); 2
+        Student sna = new Student( "Jen", LocalDate.of( 1999, 03, 23 ), 123 ); 3
+        Student sda = new Student( "Jan", LocalDate.of( 1998, 03, 23 ), 123 ); 4
+        Student sid = new Student( "Jan", LocalDate.of( 1999, 03, 23 ), 456 ); 5
+        verifyEqualsAndHashCode( ref, equ, sna, sda, sid );
+        //fail( "testMethod reached it's and. You will know what to do." );
+    }
  1. The reference object
  2. The equal object, equals true
  3. Differs in name.
  4. Differs in birth date (year).
  5. Differs in id.

The implementation of the verifyEqualsAndHashCode has been done with generics and a dash of AssertJ stuff.

Sample test helper in separate class.
    /**
+     * 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 ones differ 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() );
+        });
+     }

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.



  1. Not all equals implementation look at the type of this, See the java.util.List doc for a counter example
+ \ No newline at end of file diff --git a/docs/unit-testing-basics/index.html b/docs/unit-testing-basics/index.html index 17d8ce1..87c68f8 100644 --- a/docs/unit-testing-basics/index.html +++ b/docs/unit-testing-basics/index.html @@ -5,7 +5,8 @@ PRC2

Write your own tests!

Throughout the exercises of PRC1, you have become acquainted with the value of tests: You have a way of checking if your code is any good without having to +border-slate-500 dark:border-slate-400 text-black dark:text-slate-200">01 Unit Testing Basics

  • 02 Parameterized tests
  • Write your own tests!

    Throughout the exercises of PRC1, you have become acquainted with the value of tests: You have a way of checking if your code is any good without having to test each and every part manually. You clicked on the nice TMC button in NetBeans and then you could see which parts of your code worked, and which didn’t. There is a catch though: Out there in the real world, there won’t be any NetBeans button doing that magic for you, nor will some teacher or school provide the tests for you, so you will be on your own.

    But fret not! Writing tests is typically much simpler than writing the actual code. At least, it is when you follow a basic set of steps:

    1. Arrange: Prepare or set up the thing you want to test
    2. Act: Interact with the object you are testing
    3. Assert or Ensure that the observable result(s) is/are as expected.
    4. If it says boom, then at least you learned something …​.

    Topics week 1

    • Test Driven Development.
    • Maven configuration as exercise.
    • Arrange Act Assert
    • JUnit (5) as test framework.
    • AssertJ as assertion library.

    Testing / Test Driven Development

    What are tests and why do we need them?

    The way that you have worked with Java so far is that you had to write some code diff --git a/index.html b/index.html index d6a0dd5..b1a0322 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,8 @@ PRC2

    Module description

    • Module description can be found on canvas

    This site contains the up-to-date course material, exercises and announcements about PRC2 +border-transparent hover:border-slate-400 dark:hover:border-slate-400 text-slate-700 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200">01 Unit Testing Basics

  • 02 Parameterized tests
  • Module description

    • Module description can be found on canvas

    This site contains the up-to-date course material, exercises and announcements about PRC2 (Programming Concepts 2: the Test Driven Way), starting in February 2024.

    Responsible Teachers for 2024 PRC2:

    • Martijn Bonajo
    • Ibrahim Kouzak
    • Richard van den Ham

    Most contents on this website were originally developed by Pieter van den Hombergh.

    Study materials

    • For the Java basics and advanced features we are using Cay Horstmann’s fine books from the core Java series, 12th edition. They are on the book-list from September.
    • The exercises will be published via a personal github repository.
    • The Junit version is Junit 5.
    • "Using frameworks for testing" will start with using AssertJ, which is a modern and up to date Open Source testing framework. The documentation is of excellent quality and fun to read.
    • JavaFX bonus chapter to Core Java Volume I (for the 11th Edition.)
    • Core Java, Volume I: Fundamentals
    • Core Java, Volume II: Fundamentals

    Getting Started

    To work successfully with the programs needed for PRC2, you need to install them first. +border-transparent hover:border-slate-400 dark:hover:border-slate-400 text-slate-700 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200">01 Unit Testing Basics

  • 02 Parameterized tests
  • Getting Started

    To work successfully with the programs needed for PRC2, you need to install them first. Properly installing Java and other programs is not hard but must be done precisely. What will follow is a description on how to do that under Ubuntu Linux, macOS and Windows. You can adapt this configuration for other operating systems too, possibly with a few tweaks.

    Java Style

    Java is a language and languages come with a culture. +border-transparent hover:border-slate-400 dark:hover:border-slate-400 text-slate-700 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200">01 Unit Testing Basics

  • 02 Parameterized tests
  • Java Style

    Java is a language and languages come with a culture. One such cultural aspect is the way you format your code, like placing your brackets, parenthesis, spaces and curly braces. This coding style is intended to help you read the code, recognize the structure at a glance and find the interesting details easily and spot on.

    The official Java style is derived from the Kernighan and Ritchie (K&R) style for the C programming language. Since syntactically Java inherits quite a lot from C, that would be logical choice.

    The current and preferred way of using this java style is best described in the Google Java Style Guide.

    Where have you put your Curly Braces.

    Most holy wars are fought over the placement of the curlies '{}', and in some cases other brackets [] or parenthesis () and <> too. In particular: Put them at the beginning of the line or at the end. The Java Style Guide is quite clear about that: Braces.

    My personal motivation would be: If your understand that C and Java are block oriented languages, then you immediately will understand that placing a brace at the beginning of a line diff --git a/sitemap.xml b/sitemap.xml index 3ee1b4e..d18cdfb 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1 +1 @@ -https://fontysvenlo.github.io/prc2/pages/tips/2022-02-02T10:43:27+01:00https://fontysvenlo.github.io/prc2/docs/unit-testing-basics/2022-02-01T11:32:04+01:00https://fontysvenlo.github.io/prc2/pages/setup/2022-02-01T14:30:36+01:00https://fontysvenlo.github.io/prc2/2022-02-02T15:04:33+01:00https://fontysvenlo.github.io/prc2/pages/2022-02-02T10:43:27+01:00https://fontysvenlo.github.io/prc2/docs/2022-02-01T11:32:04+01:00https://fontysvenlo.github.io/prc2/categories/https://fontysvenlo.github.io/prc2/tags/ \ No newline at end of file +https://fontysvenlo.github.io/prc2/pages/tips/2022-02-02T10:43:27+01:00https://fontysvenlo.github.io/prc2/docs/unit-testing-basics/2022-02-01T11:32:04+01:00https://fontysvenlo.github.io/prc2/pages/setup/2022-02-01T14:30:36+01:00https://fontysvenlo.github.io/prc2/docs/parameterized-tests/2022-02-01T14:17:33+01:00https://fontysvenlo.github.io/prc2/2022-02-02T15:04:33+01:00https://fontysvenlo.github.io/prc2/pages/2022-02-02T10:43:27+01:00https://fontysvenlo.github.io/prc2/docs/2022-02-01T14:17:33+01:00https://fontysvenlo.github.io/prc2/categories/https://fontysvenlo.github.io/prc2/tags/ \ No newline at end of file