JustDummies

Just dummies — but seriously powerful ones.

Focused test values through a fluent API, for .NET.

string reference = Any.String()
                      .AlphaNumeric()
                      .InUpperCase()
                      .StartingWith("ORD-")
                      .WithLengthBetween(8, 20)
                      .Generate();
producedORD-4KMVJ2EIUA

about 1.2 MB, downloaded only if you ask

.NET CLI

dotnet add package JustDummies --prerelease

Package Manager

Install-Package JustDummies -IncludePrerelease
Find out more

The value your test does not care about

It still has to be valid.

Your test probably looks like this

[Fact]
public void A_pending_order_can_be_cancelled() {
    // Arrange
    OrderReference anyReference  = OrderReference.Create("ORD-54XEM4545");
    CustomerId     anyCustomerId = CustomerId.Create(Guid.NewGuid());
    Money          anyTotal      = Money.Create(42.00m);

    Order order = new Order(anyReference, anyCustomerId, anyTotal, OrderStatus.Pending);

    // Act
    order.Cancel();

    // Assert
    Assert.Equal(OrderStatus.Cancelled, order.Status);
}

What is this test about? A pending order can be cancelled — but you have to go looking to see it. Three of its four arrangement lines build a reference, a customer and a total that the test never mentions again: the constructor demands them, that is all. And they lie: ORD-54XEM4545 and 42.00 read as values somebody chose, when any valid ones would have done. The subject of the test is the last argument of the line that builds the order.

A first tidy-up

[Fact]
public void A_pending_order_can_be_cancelled_with_factories() {
    // Arrange
    OrderReference anyReference  = AnyOrderReference.Generate();
    CustomerId     anyCustomerId = AnyCustomerId.Generate();
    Money          anyTotal      = AnyMoney.Generate();

    Order order = new Order(anyReference, anyCustomerId, anyTotal, OrderStatus.Pending);

    // Act
    order.Cancel();

    // Assert
    Assert.Equal(OrderStatus.Cancelled, order.Status);
}

public static class AnyOrderReference {

    public static OrderReference Generate() {
        return OrderReference.Create("ORD-54XEM4545");
    }

}

// ... and AnyCustomerId and AnyMoney, which say the same thing

Better already: the factories say any, the variables say it too, and the arrangement is three lines that announce their intent. A good start — except nothing moved underneath. AnyOrderReference still returns the string it always did: the code announces any and hands you one, always the same one. The lie did not go away, it moved to another file.

Making the factory tell the truth

using JustDummies;

public static class AnyOrderReference {

    public static OrderReference Generate() {
        return OrderReference.Create(Any.String().Generate());
    }

}
refusedAn order reference must start with ORD-. (Parameter 'value')

The factory now calls the library: Any.String() draws a genuinely arbitrary string, different on every run. The name AnyOrderReference stops lying. Drawing at random is surprising, but a value you typed proves only one thing: that the test passes with that one. The domain refuses the string it drew. That string does not start with ORD-, and OrderReference.Create says so as the value is built, not three assertions later.

What the domain is asking for

public static OrderReference Create(string value) {
    ArgumentException.ThrowIfNullOrWhiteSpace(value);

    if (!value.StartsWith("ORD-", StringComparison.Ordinal)) {
        throw new ArgumentException("An order reference must start with ORD-.", nameof(value));
    }

    if (value.Length < 8) {
        throw new ArgumentException("An order reference cannot be shorter than 8 characters.", nameof(value));
    }

    if (value.Length > 20) {
        throw new ArgumentException("An order reference cannot exceed 20 characters.", nameof(value));
    }

    if (!value[4..].All(character => char.IsAsciiLetterUpper(character) || char.IsAsciiDigit(character))) {
        throw new ArgumentException("An order reference holds only uppercase letters and digits after ORD-.", nameof(value));
    }

    return new OrderReference(value);
}

These rules are not exotic, and they are written where they belong. But every one of them has to be satisfied. It is the generator's job to satisfy them, without the test having to mention any of it.

Declare the constraints, not the value

using JustDummies;

public static class AnyOrderReference {

    public static OrderReference Generate() {
        string reference = Any.String()
                              .AlphaNumeric()
                              .InUpperCase()
                              .StartingWith("ORD-")
                              .WithLengthBetween(8, 20)
                              .Generate();

        return OrderReference.Create(reference);
    }

}
produced, run after runORD-J2HLSL6DIORD-HNT3A027CEVXORD-BR5R5ABYORD-Z5VPW646GIV

Every business rule becomes a call in the chain: starts with ORD-, uppercase alphanumeric after it, between eight and twenty characters long. The value it produces changes on every run, and it is valid every time. This is where drawing at random makes sense. That value was never the subject of the test, it only had to be valid. Any value that satisfies the rules will do. You describe what the value must satisfy, not what you are going to assert.

Install it now

.NET CLI

dotnet add package JustDummies --prerelease
dotnet add package JustDummies.Xunit --prerelease

Package Manager

Install-Package JustDummies -IncludePrerelease
Install-Package JustDummies.Xunit -IncludePrerelease

Everything above is the library on its own. If that is what you came for, install it now. Take the adapter with it: it makes your draws replayable, and the page comes back to that further down. What follows shows how all this setup disappears.

Simpler still

A tool reads your type and writes the generator. The file it produces is yours.

What we would like to write

[Fact, Reproducible]
public void A_pending_order_can_be_cancelled() {
    Order order = CreateAnyPendingOrder();   // one line, and this is the one we want

    order.Cancel();

    Assert.Equal(OrderStatus.Cancelled, order.Status);
}

CreateAnyPendingOrder() replaces the three lines of arrangement, and the test says only what matters: the order is pending. You can write that helper yourself: it holds the chain of constraints you have just written, in a file of your own test project. The day Order gains a parameter, you are the one reopening that file. What follows is a tool that writes it, and rewrites it.

The tool reads your type and writes the generator

 dum generate Order
Analyzing JustDummies.SnippetValidation.Domain.Order
  constructor Order(OrderReference, CustomerId, Money, OrderStatus)

  reference   OrderReference  Any.String().WithLengthBetween(8, 20).As(OrderReference.Create)  to verify, factory, guard, unread guards
  customerId  CustomerId      Any.Guid().NonEmpty().As(CustomerId.Create)                      factory, guard
  total       Money           Any.Decimal().Positive().As(Money.Create)                        factory, guard
  status      OrderStatus     Any.Enum<OrderStatus>()

 AnyOrder.cs — 4 of 4 parameters inferred, 1 to verify.
  The file will not compile until you resolve it. That is deliberate.

dum is a global .NET tool. You run it once per type. It reads your own source and decides, parameter by parameter, how to draw a value. The last column says what it worked out on its own, and where it stopped.

A test that is explicit at last, and does not lie

[Fact, Reproducible]
public void A_pending_order_can_be_cancelled() {
    Order order = Any.Order().WithStatus(OrderStatus.Pending).Generate();

    order.Cancel();

    Assert.Equal(OrderStatus.Cancelled, order.Status);
}

Same test as before, right down to the assertion. The arrangement is one line, and that line names the only thing the test needs: the order is pending. The rest is drawn on every run, and stays valid. Nothing here is called any while handing back the same value every time.

Install all of it

.NET CLI

dotnet add package JustDummies --prerelease
dotnet add package JustDummies.Xunit --prerelease
dotnet tool install --global JustDummies.Cli --prerelease

Package Manager

Install-Package JustDummies -IncludePrerelease
Install-Package JustDummies.Xunit -IncludePrerelease

A global .NET tool is installed from the command line.

The tool is optional. The library on its own already made all of this possible. The tool only saves you the writing.

A question comes up here on its own: if the values change on every run, how do you get back the one that made a test fail?

A draw you can replay exactly

The values change on every run. The day one of them makes a test fail, you get that exact one back.

Catching a bug before it reaches production

[Fact, Reproducible]
public void A_pending_order_can_be_cancelled() {
    Order order = Any.Order().WithStatus(OrderStatus.Pending).Generate();

    order.Cancel();

    Assert.Equal(OrderStatus.Cancelled, order.Status);
}

Your build goes red although nothing changed in the code. The value drawn that day found a case your code does not hold. That is a bug that would have shipped. The draw is not lost: the next two scenes get it back exactly, in one line.

Take an example

[Fact, Reproducible]
public void A_pending_order_can_be_cancelled() {
    Order order = new AnyOrder().Generate();

    order.Cancel();

    Assert.Equal(OrderStatus.Cancelled, order.Status);
}
produced, run after runPendingCancelledCancelledShipped

This test is the same, but the status is left arbitrary. Two of the three statuses cannot be cancelled, so it goes red on roughly two runs in three. Nothing is broken. The test has just found out it was not saying what it needed.

The failing test tells you how to replay it

Ordering.Tests.OrderCancellationReplayed.A_pending_order_can_be_cancelled [FAIL]
  System.InvalidOperationException : Only a pending order can be cancelled.
  Output:
    [JustDummies] These arbitrary values were seeded with -1808250554. Reproduce this run with [Reproducible(Seed = -1808250554)].

The test that failed writes one line into your build's output. That line carries a number: the seed. That number is all it takes to draw exactly the same values again.

Paste it, and you get the same draw back

[Fact, Reproducible(Seed = -1808250554)]
public void A_pending_order_can_be_cancelled() {
    Order order = new AnyOrder().Generate();

    order.Cancel();

    Assert.Equal(OrderStatus.Cancelled, order.Status);
}
what the build got when it ran it
Ordering.Tests.OrderCancellationReplayed.A_pending_order_can_be_cancelled [FAIL]
  System.InvalidOperationException : Only a pending order can be cancelled.
  Output:
    [JustDummies] These arbitrary values were seeded with -1808250554. Reproduce this run with [Reproducible(Seed = -1808250554)].

You paste the seed your build reported into the test, and the failure comes back on your machine. These are the values that failed, not values that resemble them. Each test case draws its own seed, so a suite running in parallel hands you back the seed of the case that failed.

Want to try it?

Three packages. None of them is large, and you have seen what each one does.

.NET CLI

dotnet add package JustDummies --prerelease
dotnet add package JustDummies.Xunit --prerelease
dotnet tool install --global JustDummies.Cli --prerelease

Package Manager

Install-Package JustDummies -IncludePrerelease
Install-Package JustDummies.Xunit -IncludePrerelease

A global .NET tool is installed from the command line.

The adapter turns a red test into a draw you can replay. It is the smallest of the three packages. All three are here.