Skip to content

Commit

Permalink
Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
neboskreb committed Feb 19, 2025
1 parent 0053bff commit 6d1671e
Show file tree
Hide file tree
Showing 6 changed files with 424 additions and 2 deletions.
145 changes: 143 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,48 @@
# red-and-blue-factory-extension
# Red and Blue extension
JUnit 5 extension for easy injection of Red and Blue objects

This extension helps keeping the [data integrity](doc/problem.md) during the code evolutions. There are well-identified [scenarios of evolution](doc/bug-scenarios.md)
leading to the data corruption, and corresponding mitigations which must be applied.

# Version matching

# Solution

The code which copies/transforms the data between objects must be unit tested. But using arbitrary or random data might not always detect the issue.
For 100% detection the data known as "Red" and "Blue" objects should be used.

## What are "Red" and "Blue" objects?

Two objects of the same class having in each of their fields a value non-equal to the value of this field in the other object,
are known as [Red and Blue objects](doc/red-and-blue.md) of that class.

## What they are used for?

They are essential for checking the correctness and completeness of data copying, e.g. in a mapper between a DTO and entity.
Typical areas of failure and testing strategies are described in chapter [What should be tested](doc/what-to-test.md).

While you can create such objects manually, letting this extension inject them for you is much easier.


# Setup

Maven
```xml
<dependency>
<groupId>io.github.neboskreb</groupId>
<artifactId>red-and-blue</artifactId>
<version>1.2.0</version>
<scope>test</scope>
</dependency>
```

Gradle
```groovy
dependencies {
testImplementation 'io.github.neboskreb:red-and-blue:1.2.0'
}
```

## Version matching
Red-and-Blue Extension relies on internal object factories from [EqualsVerifier library](https://github.com/jqno/equalsverifier) to populate the objects. This internal API might change between the versions.
Hence, newer versions of Red-and-Blue extension are not compatible with older versions of EqualsVerifier, and vice versa.

Expand All @@ -11,3 +51,104 @@ Hence, newer versions of Red-and-Blue extension are not compatible with older ve
| 1.1.0 | < 3.17 |
| 1.2.0 | >= 3.17 |
| 1.3.0 | >= 3.19 |



# Usage

## Applying the extension

Enable the extension on your JUnit5 test class:
```java
@ExtendWith(RedAndBlueExtension.class)
class MyClassTest {
...
}
```

## Injection

Inject Red and Blue instances into your test method as parameters:
```java
@ExtendWith(RedAndBlueExtension.class)
class MyClassTest {
@Test
void testParameterInjection(@RedInstance MyClass red, @BlueInstance MyClass blue) {
...
}
}
```

Or inject into fields:
```java
@ExtendWith(RedAndBlueExtension.class)
class MyClassTest {

@RedInstance
private MyClass red;

@BlueInstance
private MyClass blue;
...
}
```

## Prefabricating

In cases when automatic creation of instances is not possible (e.g., when the class contains circular reference to itself), you can provide
prefabricated instances - one for Red and one for Blue:
```java
public class MyComplexClass {
private TrickyClass tricky;
...
}

// Assume that, for some reason, TrickyClass cannot be created automatically.
// Provide prefabricated instances:

@ExtendWith(RedAndBlueExtension.class)
class MyComplexClassTest {

// Provide the instance directly:
@PrefabRed
private TrickyClass trickyRed = new TrickyClass(...);

// Or via a factory method:
@PrefabBlue
private static TrickyClass trickyProvider() {
return new TrickyClass(...);
}

// Now you can inject MyComplexClass as usual:
@Test
void testParameterInjection(@RedInstance MyComplexClass red, @BlueInstance MyComplexClass blue) {
...
}
}
```

## Tests structure

Inheriting fields and methods from a base class is supported:
```java
abstract class MyClassTestBase {
@RedInstance
protected MyClass red;
@BlueInstance
protected MyClass blue;
}

@ExtendWith(RedAndBlueExtension.class)
class MyClassTest extends MyClassTestBase {
@Test
void testBaseParameterInjection() {
// You can use the inherited fields `red` and `blue` here
assertEquals(..., red);
assertEquals(..., blue);
}
}
```


# Contribution
Pull requests are welcome! If you plan to open one, please first create an issue where you describe the problem/gap your contribution closes, and tag the keeper(s) of this repo so they could get back to you with help.
79 changes: 79 additions & 0 deletions doc/bug-scenarios.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Scenarios of failure

Below are the known scenarios how evolutions of your code, if not mitigated, result in loss of data or degradation of performance.

Module [examples](/examples) demonstrates these scenarios. The code contains `TODO [x]` items which, when commented or uncommented, simulate the evolution
leading to a bug. After un-/commenting the line, rerun the unit tests to see which will detect the bug.

------------------------------------------------------------------------------------------------------

## Incomplete copy constructor/builder

**Risk:** data loss

**Scenario:** When a new field is added to a class which has a copy constructor/copy builder without having the latter updated, this field will not be copied.

**Demonstration:** Uncomment line `TODO [3]` to demonstrate this bug.

**Mitigation:** build the copy and test it against the original field-by-field
```java
ImmutableEntity copy = new ImmutableEntity.Builder(original).build();
assertThat(copy).as("Copy builder is incomplete").usingRecursiveComparison().isEqualTo(original);
```

**Example:** [CopyBuilderTest](/examples/src/test/java/com/github/neboskreb/red/and/blue/example/CopyBuilderTest.java)

------------------------------------------------------------------------------------------------------

## Incomplete mapper

**Risk:** data loss

**Scenario:** When a generated mapper is not defined properly, or a manually written mapper is incomplete, some fields will not be copied.

**Demonstration:** Comment line `TODO [1]` and/or `TODO [2]` to demonstrate this bug.

**Mitigation:** use reflective mapping to obtain the copy and test it against the original field-by-field
```java
Entity copy = mapper.toEntity(mapper.toDTO(entity));
assertThat(copy).as("Mapper is incomplete").usingRecursiveComparison().isEqualTo(entity);
```

**Example:** [MapperTest](/examples/src/test/java/com/github/neboskreb/red/and/blue/example/MapperTest.java)

------------------------------------------------------------------------------------------------------

## Incomplete DTO / DAO

**Risk:** data loss

**Scenario:** When a field is added to the Entity object but the DTO/DAO is not updated accordingly, this field will not be transferred/stored.

**Demonstration:** Comment line `TODO [6]` to demonstrate this bug.

**Mitigation:** use reflective mapping to obtain the copy and test it against the original field-by-field
```java
Entity copy = mapper.toEntity(mapper.toDTO(entity));
assertThat(copy).as("DTO is incomplete").usingRecursiveComparison().isEqualTo(entity);
```

**Example:** [MapperTest](/examples/src/test/java/com/github/neboskreb/red/and/blue/example/MapperTest.java)

------------------------------------------------------------------------------------------------------

## Excessive DTO

**Risk:** performance degradation

**Scenario:** In a client-server setting, when a field is removed from the Entity object on the client's end but the DTO is not updated accordingly,
the server will still populate this field, effectively wasting the CPU cycles and the bandwidth.

**Demonstration:** Uncomment line `TODO [5]` to demonstrate this bug.

**Mitigation:** use reflective mapping to obtain the copy of DTO and test it against the original DTO field-by-field
```java
EntityDTO dtoCopy = mapper.toDTO(mapper.toEntity(dto));
assertThat(dtoCopy).as("DTO is excessive").usingRecursiveComparison().isEqualTo(dto);
```

**Example:** [MapperTest](/examples/src/test/java/com/github/neboskreb/red/and/blue/example/MapperTest.java)
65 changes: 65 additions & 0 deletions doc/faulty-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Example of code evolution

Assume you have class A, and a new field `newField` is added. But due to a human mistake, only the normal constructor is updated but the copy constructor is not:
```java
public class A {
private int oldField;
private boolean newField;

public A(int oldField, boolean newField) {
this.oldField = oldField;
this.newField = newField;
}

/** Copy constructor. */
public A(A original) {
this.oldField = original.oldField;
// Note that `newField` is not copied. It's a bug!
}

@Override
public final boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof A otherA)) return false;

return oldField == otherA.oldField && newField == otherA.newField;
}
}
```

If your test has only one check, there is a big risk such bug will go undetected:
```java
@Test
void incorrect() {
// GIVEN
A original = new A(1, false);

// WHEN
A copy = new A(original);

// THEN
assertEquals(original, copy);
}
```

You need two checks to reliably detect this bug:
```java
@Test
void correct() {
// GIVEN
A originalRed = new A(1, false);
A originalBlue = new A(2, true);

// WHEN
A copyRed = new A(originalRed);
A copyBlue = new A(originalBlue);

// THEN
assertEquals(originalRed, copyRed);
assertEquals(originalBlue, copyBlue);
}
```

So you need two objects having every field different from its counterpart. Such two objects are known as "Red" and "Blue" objects. Each field in Red object is non-equal to this field in Blue object.

This extension offers an easy way of creating the Red and Blue objects of arbitrary class.
43 changes: 43 additions & 0 deletions doc/problem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

# How the data loss occurs

In the beginning, all works well and no data is lost. But no code is carved in stone - after written, it evolves and mutates as the project develops.
Sometimes a seemingly benign change to a data object, like an added field, can result in data loss or performance degradation. Not every such case is
easily detected by the unit tests.

One infamous example is the `equals` method: adding a new field to the class without having the `equals` updated is notoriously
difficult to catch. Even if your test manually checks each field it will stay green - thus failing to detect the bug.
This happens because your test simply doesn't know that something was added, hence doesn't check it!
For this reason, libraries like EqualsVerifier were created to automatically find and check fields.

# Path to the pitfall

But `equals` is not the only scenario. Other areas of risk include:
* builders
* mappers
* copiers
* mutator methods
* etc.

All of the above can result in a data loss if the bug is not caught in time.

**Data loss is not a joke. You want to be protected against it.**

# Solution

Cover your mappers/builders/etc with unit tests.

For this you will need two instances of each class under test. Yes, two - "Red" and "Blue".

# Why one simple `equals` check is not enough?

Because `equals` only tests that fields are equal but doesn't assure that the field has been copied. If in your test the original's
field had default value (i.e., 0 for `int` or `false` for `boolean`) the `equals` doesn't see a fault because the copy's field is
initialized to the same default value, so they match.

To catch this bug you need to check the filed **two times** with different values: this way either the first or second check will
catch the discrepancy (because a type can have only one default value).

Here is the [example](faulty-example.md) demonstrating how the single check fails but the dual check wins.

**One check is not enough. You need two.**
28 changes: 28 additions & 0 deletions doc/red-and-blue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

# What are "Red" and "Blue" objects?

Objects known as "Red" and "Blue" are two instances of the same class which have each field different from the same field in their counterpart.

E.g. if class contains two String fields and one int field, like this,
```java
public class A {
private String first;
private String second;
private int third;
}
```
then the red and blue instances could be like these:
<pre><code>
| red | blue |
|----------------------|----------------------|
| { | { |
| "first": "one", | "first": "two", |
| "second": "one", | "second": "two", |
| "third": 1000 | "third": 2000 |
| } | } |
</code></pre>

_NOTE that fields `first` and `second` are not required to differ from each other in the same instance._



Loading

0 comments on commit 6d1671e

Please # to comment.