Coder Social home page Coder Social logo

expectbetter's Introduction

Expect Better

An expressive and fluent assertion library for C#.

Build Status

Better Unit Testing for .NET

Would you like your assertions to be shorter and more expressive? NUnit gets us far, but Assert is limited in what it can express - you can't add to it. xUnit gets us a little farther, but has the same problem. We can do better:

Expect.The(stringValue).Not.ToBeNullOrEmpty();
Expect.The(method).ToThrow<DivideByZeroException>();
Expect.The(response).ToBeBadRequest();
Expect.The(listOfStuff).ToContainInOrder("foo", "bar", "qux");

ExpectBetter allows you to write tests that say what you mean while still using the frameworks you already know. Taking a cue from great-expectations and Hamcrest, ExpectBetter lets you compose matchers for the types you own. What's more, matchers both built-in and bespoke are always at your fingertips via type inferencing and IDE autocompletion - you'll never again need to resort to documentation to discover assertions.

Example

Here's what ExpectBetter looks like when used in an NUnit fixture (excerpted from this project):

using System;

using NUnit.Framework;

using ExpectBetter;
using ExpectBetter.Matchers;

namespace ExpectBetterTests.Matchers
{
    [TestFixture]
    public class StringMatcherTest
    {
        string actual;
        StringMatcher matcher;

        [SetUp]
        public void Setup()
        {
            actual = Factory.RandomString(20, 5);
            matcher = Expect.The(actual);
        }

        [Test]
        public void ToContain_WhenExpectedIsContained_ReturnsTrue()
        {
            var expectedLen = Math.Min(actual.Length / 2 + 1, actual.Length);
            var expected = actual.Substring(expectedLen);
            var result = matcher.ToContain(expected);

            Expect.The(result).ToBeTrue();
        }
    }
}

Get Started

If you use NuGet: Install-Package ExpectBetter in the Package Management Console for your test projects.

If you don't use NuGet, please consider using it. After your consideration, you can check out this repository and build it.

If you'd rather not build it, you can download the library itself here at the Github project site:

Once you have the library, add a reference to ExpectBetter.dll to your test projects, add the ExpectBetter namespace, and get to testing!

Expectations

In ExpectBetter, assertions are framed as an expectation - you expect that an object has certain properties. If that expectation is not met, an ExpectationException informs you of the fact.

Any expectation you can state has a logical negation - if you can say "expect the list to be null", you can just as easily say "expect the list not to be null." Here's what that looks like in ExpectBetter:

object nullObj = null,
       otherObj = new object();

Expect.The(nullObj).ToBeNull();
Expect.The(otherObj).Not.ToBeNull();

Your First Tests

ExpectBetter works seamlessly with your existing unit test framework. "Arrange" and "Act" as you normally would, but instead of asserting... expect!

// Arrange
var dict = new Dictionary<string, string>();

// Act
dict["foo"] = "bar";
dict["baz"] = "quux";

// Expect
Expect.The(dict).Not.ToBeEmpty();

There are built-in matchers for many common data types, including IEnumerable, ICollection, IDictionary<K, V>, DateTime, string, numeric types, and many more; auto-completing IDEs will show you all applicable test methods based on type inference.

Writing Tests

Add ExpectBetter assertions to your current unit test framework - in NUnit, for example:

using ExpectBetter;

[TestFixture]
public class Tests
{
    private FileInfo file;
    
    // some setup
    
    [Test]
    public void TestFileExists()
    {
        Expect.The(file).ToExist();
    }
    
    [Test]
    public void TestFileIsNotEmpty()
    {
        var contents = file.ReadAllText();
        Expect.The(contents).Not.ToBeNullOrEmpty();
    }
}

Writing Matchers

Matchers are simple to write. The only restrictions are that they must inherit from ExpectBetter.BaseMatcher<T,M>, and their test methods need to be virtual and return bools. Other than that, your imagination is the limit.

public class FileInfoMatcher : BaseMatcher<FileInfo, FileMatcher>
{
    public virtual bool ToExist()
    {
        return actual.Exists;
    }
    
    public virtual bool ToBeEmpty()
    {
        return actual.Length == 0;
    }

    public virtual bool ToHaveBeenModifiedSince(DateTime expected)
    {
        actual.Refresh();
        
        if (expected.Kind == DateTimeKind.Utc)
        {
            return actual.LastWriteTimeUtc > expected;
        }

        return actual.LastWriteTime > expected;
    }
}

There's some boilerplate to write, unfortunately - you'll need to subclass ExpectBetter.Expect, and add a static method that returns your new matchers wrapped in some magic.

public class Expect : ExpectBetter.Expect
{
    public static FileInfoMatcher The(FileInfo actual)
    {
        return Expectations.Wrap<FileInfo, FileInfoMatcher>(actual);
    }
}

Now use your new Expect class instead of ExpectBetter.Expect, and your matcher will be available with all the rest in your tests.

Customizing Messages

The default error message is generated from the name of your test method; it follows this pattern: "Expected [actual] [method] [expected]". For example, if ToHaveBeenModifiedSince returns false, the error message would be: Failure: Expected [System.FileInfo] to have been modified since [2012-01-01]. Descriptions of actual and expected values are generated with a call to .ToString(). That System.FileInfo part isn't so informative - what if you want a better message? Set the BaseMatcher<T, M>.actualDescription field.

Let's try:

public virtual bool ToHaveBeenModifiedSince(DateTime expected)
{
    this.actualDescription = actual.FullName;
    // as above
}

Now the failure message might read: Failure: Expected [C:\autoexec.bat] to have been modified since [2012-01-01].

You can also customize the "expected" description by setting the expectedDescription field. Should you want to provide your own message entirely, the failureMessage field allows you to override all message generation.

FAQ

How does it work?

Generics, and runtime code generation courtesy of Castle.DynamicProxy.

Basically, the mechanics of assertions are the same no matter what the language or framework - check a condition, then report an error if the condition doesn't hold. These mechanics are provided at runtime by a generated wrapper class around each matcher, the result of a call to Expectations.Wrap<TActual, TMatcher>(TActual actual). The behavior of generated wrappers is straightforward - it implements a null check for actual, invokes the test method, checks the result, and perhaps constructs a meaningful failure message and raises an error.

This code requires sufficient .NET privileges to define and run dynamic assemblies; if your test environment has tighter constraints, you may encounter problems.

What if null is a legal value for my object?

ExpectBetter is opinionated and dislikes null; it would much prefer that some form of Option<T> be used instead. Nevertheless it respects that others feel differently, and so you can add an [AllowNullActual] attribute to your test methods. With this in place, you will no longer get failures when your tested value is null.

Next Steps

  • Better integration with test frameworks - specify exception type thrown, etc.

Credit Where Credit Is Due

This is an attempt at a .NET port of jasmine and great-expectations, keeping true to the expressiveness of Jasmine, the generics finesse of great-expectations, and to the spirit and practice of .NET development.

expectbetter's People

Contributors

benjamin-bader avatar mortonfox avatar wildmichael avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

wildmichael

expectbetter's Issues

There is no way to assert enumerable equivalence

Say I have two enumerables:

var a = new[] { 1, 2, 3 };
var b = new[] { 1, 2, 3 };

I don't think that currently there is a single statement that asserts that both sequences contain the same values in order. I suppose a combination of ToNumber(), ToContainInOrder() and ToContainExactly() would do the trick, but using multiple assertions to express a single expectation seems to be wrong and error prone.

In fact, this is what I expected ToContainExactly() to do, but had to find out that it only operates in the sense of a set or a bag, which is frankly quite confusing if used on ordered sequences.

I guess I would like a shortcut for

Expect.The(Enumerable.SequenceEqual(a, b)).ToBeTrue();

that is a bit more expressive.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.