Thursday, 21 September 2017

NExpect level 2: testing collections

In a prior post, I covered simple value testing with NExpect. In this post, I'd like to delve into collection assertions, since they are fairly common.

First, the simplest: asserting that a collection contains a desired value:


  [Test]
  public void SimpleContains()
  {
    // Arrange
    var collection = new[] { "a", "b", "c" };
  
    // Assert
    Expect(collection).To.Contain("a");
  }

This is what you would expect from any other assertions framework.

Something has always bothered me about this kind of testing though. In particular, the test above passes just as well as this one:


  [Test]
  public void MultiContains()
  {
    // Arrange
    var collection = new[] { "a", "b", "c", "a" };
  
    // Assert
    Expect(collection).To.Contain("a");
  }

And yet they are not functionally equivalent from where I stand. Which makes the test feel a little flaky to me. This is why NExpect actually didn't even have the above assertion first. Instead, I was interested in being more specific:

  [Test]
  public void SpecificContains()
  {
    // Arrange
    var collection = new[] { "a", "b", "c", "a" };
  
    // Assert
    Expect(collection).To.Contain.Exactly(1).Equal.To("b");
    Expect(collection).To.Contain.At.Least(1).Equal.To("c");
    Expect(collection).To.Contain.At.Most(2).Equal.To("a");
  }

Now my tests are speaking specifically about what they expect.

Sometimes you just want to test the size of a collection, but you don't really care if it's an IEnumerable<T>, a List<T> or an array. Other testing frameworks may let you down, requiring you to write a test against the Count or Length property, meaning that when your implementation changes from returning, eg, List<T> to array (which may be smart: List<T> is not only a heavier construct but implies that you can add to the collection), your tests will fail for no really good reason -- your implementation still returns 2 FrobNozzles, so who cares if the correct property to check is Length or Count? I know that I don't.

That's Ok, NExpect takes away the care of having to consider that nuance and allows you to spell out what you actually mean:

  [Test]
  public void SizeTest()
  {
    // Arrange
    var collection = new[] { "a", "b", "c" };
    var lonely = new[] { 1 };
    var none = new bool[0];

    // Assert
    Expect(collection).To.Contain.Exactly(3).Items();
    Expect(lonely).To.Contain.Exactly(1).Item();

    Expect(none).To.Contain.No().Items();
    Expect(none).Not.To.Contain.Any().Items();
    Expect(none).To.Be.Empty();
  }

Note that the last three are functionally equivalent. They are just different ways to say the same thing. NExpect is designed to help you express your intent in your tests, and, as such, there may be more than one way to achieve the same goal:

  [Test]
  public void AnyWayYouLikeIt()
  {
    // Assert
    Expect(1).Not.To.Equal(2);
    // ... is exactly equivalent to
    Expect(1).To.Not.Equal(2);

    Expect(3).To.Equal(3);
    // ... is exactly equivalent to
    Expect(3).To.Be.Equal.To(3);
  }

There are bound to be other examples. The point is that NExpect attempts to provide you with the language to write your assertions in a readable manner without enforcing a specific grammar.

Anyway, on with collection testing!

You can test for equality, meaning items match at the same point in the collection (this is not reference equality testing on the collection, but would equate to reference equality testing on items of class type or value equality testing on items of struct type:

  [Test]
  public void CollectionEquality()
  {
    // Assert
    Expect(new[] { 1, 2, 3 })
      .To.Be.Equal.To(new[] { 1, 2, 3 });
  }

You can also test out-of-order:

  [Test]
  public void CollectionEquivalence()
  {
    // Assert
    Expect(new[] { 3, 1, 2 })
      .To.Be.Equivalent.To(new[] { 1, 2, 3 });
  }

Which is all nice and dandy if you're testing value types or can do reference equality testing (or at least testing where each object has a .Equals override which does the comparison for you). It doesn't help when you have more complex objects -- but NExpect hasn't forgotten you there: you can do deep equality testing on collections too:

  [Test]
  public void CollectionDeepEquality()
  {
    var input = new[] {
      new Person() { Id = 1, Name = "Jane", Alive = true },
      new Person() { Id = 2, Name = "Bob", Alive = false }
    };
    // Assert
    Expect(input.AsObjects())
      .To.Be.Deep.Equal.To(new[] 
        {
          new { Id = 1, Name = "Jane", Alive = true },
          new { Id = 2, Name = "Bob", Alive = false }
        });
  }

Note that, much like the points on "Who's line is it, anyway?", the types don't matter. This is deep equality testing (: However, we did need to "dumb down" the input collection to a collection of objects with the provided .AsObjects() extension method so that the test would compile, otherwise there's a type mismatch at the other end. Still, this is, imo, more convenient than the alternative: item-for-item testing, property-by-property.

The above is incomplete without equivalence, of course:

  [Test]
  public void CollectionDeepEquivalence()
  {
    var input = new[] {
      new Person() { Id = 1, Name = "Jane", Alive = true },
      new Person() { Id = 2, Name = "Bob", Alive = false }
    };
    // Assert
    Expect(input.AsObjects())
      .To.Be.Deep.Equivalent.To(new[] {
        new { Id = 2, Name = "Bob", Alive = false },
        new { Id = 1, Name = "Jane", Alive = true }
      });
  }

And intersections are thrown in for good measure:

  [Test]
  public void CollectionIntersections()
  {
    var input = new[] {
      new Person() { Id = 1, Name = "Jane", Alive = true },
      new Person() { Id = 2, Name = "Bob", Alive = false }
    };
    // Assert
    Expect(input.AsObjects())
      .To.Be.Intersection.Equivalent.To(new[] {
        new { Id = 2, Name = "Bob" },
        new { Id = 1, Name = "Jane" }
      });
    Expect(input.AsObjects())
      .To.Be.Intersection.Equivalent.To(new[] {
        new { Id = 1, Name = "Jane" },
        new { Id = 2, Name = "Bob" }
      });
  }

You can also test with a custom IEqualityComparer<T>:

  [Test]
  public void CollectionIntersections()
  {
    var input = new[] {
      new Person() { Id = 1, Name = "Jane", Alive = true },
      new Person() { Id = 2, Name = "Bob", Alive = false }
    };
    // Assert
    Expect(input)
      .To.Contain.Exactly(1).Equal.To(
        new Person() { Id = 2, Name = "Bob" }, 
        new PersonEqualityComparer()
    );
  }

or with a quick-and-dirty Func<T>:

  [Test]
  public void CollectionIntersections()
  {
    var input = new[] {
      new Person() { Id = 1, Name = "Jane", Alive = true },
      new Person() { Id = 2, Name = "Bob", Alive = false }
    };
    // Assert
    Expect(input.AsObjects())
      .To.Contain.Exactly(1).Matched.By(
        p => p.Id == 1 && p.Name == "Jane"
      );
  }

And all of this is really just the start. The real expressive power of NExpect comes in how you extend it.

But more on that in the next episode (:

No comments:

Post a Comment

PeanutButter.RandomValueGen: the builder pattern & random generation for testing purposes

Retrieving the post... Please hold. If the post doesn't load properly, you can check it out here: https://github.com/fluffynuts/blog/...