Assumptions

The Assumptions library provides a clean method to make implicit assumptions explicit in C#.


Keywords
assumptions, assume, assert, implicit, explicit, code, quality, testing
License
Unlicense
Install
Install-Package Assumptions -Version 1.2.0

Documentation

Assumptions

The Assumptions library provides a clean method to replace implicit assumptions with explicit ones in C#.

Assumptions are much akin to Asserts in unit tests, but differ in that they are intended to be part of production code.

1. Basic usage

An Assumption is typically a statement such as "assume that 'foo' is equal to 'blat'.

In fact, since the Assumption library uses a the fluent interface pattern, the syntax is pretty much just that:

Assume.That(foo).Is.Equal.To("blat", "Foo should have had the value 'blat'")

2. Starting an Assumption

Assumptions always start with one of the following:

Starts with Description
Assume.That(actual) Followed by a logical operation comparing the actual value to an expected value.
Assume.That(x => {}) Followed by an expression regarding the expected execution state of 'x'
Assume.Unreachable("reason") This expression is a special case, which will always raise an exception and does not support method chaining.

The result of Assume.That will be an Assumption object, which should be further appended to via method chaining.

3. Stating the relationship

Whereas the first two words in an assumption will generally be Assume.That, declaring the actual value, the next word will typically state the relationship that should exist between the actual value and the expected value.

The following relationships are supported:

Relationship Description
Equal.To(expected) actual == expected
Less.Than(expected) actual < expected
Greater.Than(expected) actual > expected
LessThanOrEqual.To(expected) actual <= expected
GreaterThanOrEqual.To(expected) actual >= expected
Empty() string.IsNullOrEmpty(actual) for strings
Empty() !((ICollection)actual).Any() for collections
Empty() false == ((IEnumerable)actual).GetEnumerator().MoveNext() for enumerables
Null() actual == null
True() actual == true
False() actual == false
InstanceOf(expected) typeof(expected).IsAssignableFrom(actual) if expected is a Type
InstanceOf(expected) expected.Any(x => typeof(x).IsAssignableFrom(actual)) if expected is a Type[]

4. Negating a relationship

Any relationship can be negated by preceeding it with a Not operator, e.g.

Assume.That(true).Is.Not.False();

Double-negatives are not supported.

5. Special case: the Completed relationship

The 'completed' relationship can be used to test that a lambda completes without throwing an exception, e.g.:

Assume.That((a) => { throw new NotSupportedException(); }).Not.Completed();

6. Terminating words

Whereas the first two words in an assumption will generally be Assume.That, declaring the actual value, the last word will be a terminating word.

When a terminating word is encountered, the expression will be evaluated as it stands at that point in time, raising an AssumptionFailure if the actual value does not have the prescribed relationship to the expected value.

The following words terminate an assumption:

Terminator Description
Than(expected) Terminates a comparison or equality relationship, such as Less or Equal
To(expected) Synonymous with Than
Empty() Tests a string or collection for emptiness
Null() Synonymous with To(null)
True() Synonymous with To(true)
False() Synonymous with To(false)
InstanceOf(expected) Terminates a type-check-relationship

7. Chained assumptions

Assumptions can be chained together with the And operator. The original actual value, declared in That(x), will stil apply, e.g.

Assume.That(x).Is.Greater.Than(y).And.Less.Than(z)

A new That statement may however be issued as well to change the actual value, e.g.

Assume.That(x).Is.Greater.Than(y).And.That(x + 100).Is.Less.Than(z)

8. Non-trivial words

Following 'That', every property/call will return a modified version of the Assumption instance returned from 'That'. Trivial words will have effects or side-effects, while there are also some convenient non-trivial words that you may use with no other effect than to increase readability.

The non-trivial words supported by the Assumptions library are:

  • Is, Be, An, Of, A, Been, Have, Has, With, Which, The, It

9. Explanations

By default, an AssumptionFailure will have a stock message explaining the condition that was amiss, e.g.

var x = 10;
var y = 12;

try
{
    Assume.That(x).Is.Greater.Than(y);
}
catch (AssumptionFailure ex)
{
    Assert.Equal("Expected '10' to be greater than '12'", ex.Message);
}

Optionally, an explanatory message can be added to the assumption's terminating word, e.g.

var x = 40;
var y = 42;

try
{
    Assume.That(x).Is.Equal.To(y, "The answer to the ultimate question is 42");
}
catch (AssumptionFailure ex)
{
    Assert.Equal("The answer to the ultimate question is 42. Expected '40' to be equal to '42'", ex.Message);
}

Note that each terminating word in a chained expression should provide its own explanation.

Example:

Assert.That(a).Has.Been.Less.Than(b);

A. Detailed Topics

A1. Implicit vs explicit assumptions

Implicit assumptions

Implicit assumptions are made when results are not checked, branches are not included, exceptions are not caught, collections are assumed to contain elements, and so forth.

Let as explore a common example:

Example: Missing null checks

When a piece of code assumes that a reference is not null by deferencing without testing for validity, then an assumption is implicitly made that the reference is indeed valid.

Typically, this assumption may hold. If it does not however, then a NullReference will be thrown.

While this may appear at face value to be less severe than an access violation, there is little difference in the eyes of the user who's application failed.

// pattern 1: a typical missing null check
var foo = Blat();
var splat = foo.Bar();
splat.Bark();

This code can of course be patched up by making the implicit explicit, and making the code more fault tolerant, e.g.:

// pattern 2: a typical fault tolerate null check addition
var foo = Blat();
if (foo != null)
{
    var splat = foo.Bar();
    if (splat != null)
    {
        splat.Bark();
    }
}

This gets pretty verbose pretty fast, so people tended to skip this pattern rather frequently. Also there is now no indication that anything is amiss. That may e.g. be remedied with logging:

// pattern 3: a typical fault tolerate null check addition, with logging
var foo = Blat();
if (foo != null)
{
    var splat = foo.Bar();
    if (splat != null)
    {
        splat.Bark();
    }
    else
    {
        log.Warn("splat was null!");
    }
}
else
{
    log.Warn("Foo was null!");
}

We could also have gone all-in on this code and made the holes more explicit. This would be more informative when we sift through our logs, but gets extremely verbose extremely fast:

// pattern 3: a typical robust, explicit, fault intolerant solution
var foo = Blat();
if (foo == null)
{
    throw new BlatException("Foo was null!");
}

var splat = foo.Bar();
if (splat == null)
{
    throw new BlatBarException("Bar returned null.  Perhaps the code is fubar?");
}
splat.Bark();

This is certainly more robust, and can be considered more explanatory. But now its hard for the caller to know whether the exceptions are plausible cases or highly abnormal. And this is a bit repetitive, ugly, and verbose. Its not really a very sustainable coding style, and it is costly both from a readability and writability perspective.

So typically code was written more frequently according to pattern 1. So frequently in fact that C# has introduced syntactic sugar to make it easier to at least "level up" to pattern 2, where there is some level of fault tolerance. Now were roughly back where we started, syntactically speaking, and we're not throwing cryptic NullReference exceptions to boot:

// pattern 4: a fault-tolerant implementation using the ?. shorthand.
var foo = Blat();
var splat = foo?.Bar();
splat?.Bark();

But now we also have to henceforth deal with the fact that foo and splat may indeed be null, and we may or may not have invoked foo.Bar() and/or splat.Bark(). Are we better off? Perhaps. Perhaps sometimes. Perhaps sometimes not.

Every time we write code, we make assumptions. For the most part, our assumptions hold true. But when they don't... We experience bugs, crashes, cryptic log messages, incomprehensible metrics, generic null reference exceptions, division by zero, exceptions in code that has no exception handling, and many more indications of the issue.

These indicators are the effect, not the cause.

The cause is typically the same: Somewhere, someone made an incorrect, implicit assumption.

Explicit assumptions

The Assumptions library allows you to transform implicit assumptions into explicit ones, offering an alternative solution.

// pattern 5: Explicit assumptions
var foo = Blat();
Assume.That(foo).Is.Not.Null("Foo was null, which was highly unexpected and implies that the Blat library is broken");

var splat = foo.Bar();
Assume.That(splat).Is.Not.Null("There was no splatter available for fubarring");

splat.Bark();

The added value in this code is that the reader knows that we have thought of the edge cases. It may be verbose, but we have codified our knowledge, provided specific explanations which will presumably reach our logs. And we have expressed to the reader that these are not natural conditions which the caller is expected to provide exception handling for.

A few more examples

Here are a few more examples, for inspiration:

void Foo(string bar)
{
    Assume.That(bar).Is.Not.Null();

    Console.WriteLine($"Explanation: {bar}")
}

void Fu(int bar)
{
    Assume.That(bar).Is.Not.Equal.To(4);

    return fu / (bar - 4);
}

void Transfer(string fromAccount, string toAccount, int amount)
{
    // the client would not have issued this call naturally if this were not the case, but we still better check this, in case of hackers

    var owner = OwnerOf(fromAccount);
    var user = CurrentUser();
    Assume.That(user).Is.Equal.To(owner);

    Assume.That(fromAccount).Is.Not.Equal.To(toAccount);
    Assume.That(amount).Is.Greater.Than(0);
}