Thursday, 21 September 2017

NExpect level 1: testing objects and values

I recently introduced NExpect as an alternative assertions library. I thought it might be nice to go through usage, from zero to hero.

NExpect is available for .NET Framework 4.5.2 and above as well as anything which can target .NET Standard 1.6 (tested with .NET Core 2.0)

So here goes, level 1: testing objects and values.

NExpect facilitates assertions (or, as I like to call them: expectations) against basic value types in a fairly unsurprising way:


  [Test]
  public void SimplePositiveExpectations
  {
    // Arrange
    object obj = null;
    var intValue = 1;
    var trueValue = true;
    var falseValue = false;

    // Assert
    Expect(obj).To.Be.Null();
    Expect(intValue).To.Equal(1);
    Expect(trueValue).To.Be.True();
    Expect(falseValue).To.Be.False();
  }

So far, nothing too exciting or unexpected there. NExpect also caters for negative expectations:

  [Test]
  public void SimpleNegativeExpectations
  {
    // Arrange
    object obj = new object();
    var intValue = 1;
    var trueValue = true;
    var falseValue = false;

    // Assert
    Expect(obj).Not.To.Be.Null();
    Expect(intValue).Not.To.Equal(2);
    Expect(trueValue).Not.To.Be.False();
    Expect(falseValue).Not.To.Be.True();

    Expect(intValue).To.Be.Greater.Than(0);
    Expect(intValue).To.Be.Less.Than(10);
  }

(Though, in the above, I'm sure we all agree that the boolean expectations are neater without the .Not).

Expectations carry type forward, so you won't be able to, for example:

  [Test]
  public void ExpectationsCarryType
  {
    Expect(1).To.Equal("a");  // does not compile!
  }

However, expectations around numeric values perform upcasts in much the same way that you'd expect in live code, such that you can:


  [Test]
  public void ShouldUpcastAsRequired()
  {
    // Arrange
    int a = 1;
    byte b = 2;
    uint c = 3;
    long d = 4;

    // Assert 
    Expect(b).To.Be.Greater.Than(a);
    Expect(c).To.Be.Greater.Than(b);
    Expect(d).To.Be.Greater.Than(a);
  }

All good and well, but often we need to check that a more complex object has a bunch of expected properties.

.Equal is obviously going to do reference-equality testing for class types and value equality testing for struct types. We could:

  [Test]
  public void TestingPropertiesOneByOne()
  {
    // Arrange
    var person = new {
      Id = 1,
      Name = "Jane",
      Alive = true
    };

    // Assert
    Expect(person.Id).To.Equal(1);
    Expect(person.Name).To.Equal("Jane");
    Expect(person.Alive).To.Be.True();
  }

But that kind of test, whilst perfectly accurate, comes at a cognitive overhead for the reader. Ok, perhaps not much overhead in this example, but imagine if that person had come from another method:

  [Test]
  public void TestingPropertiesOneByOne()
  {
    // Arrange
    var sut = new PersonRepository();
    
    // Act
    var person = sut.FindById(1);

    // Assert
    Expect(person).Not.To.Be.Null();
    Expect(person.Id).To.Equal(1);
    Expect(person.Name).To.Equal("Jane");
    Expect(person.Alive).To.Be.True();
  }

In this case, we'd expect the result to also have a defined type, not some anonymous type. It would be super-convenient if we could do deep equality testing. Which we can:

  [Test]
  public void DeepEqualityTesting()
  {
    // Arrange
    var sut = new PersonRepository();
    
    // Act
    var person = sut.FindById(1);

    // Assert
    Expect(person).To.Deep.Equal(new {
      Id = 1,
      Name = "Jane",
      Alive = 1
    });
  }
This exposes our test for what it's really doing: when searching for the person with the Id of 1, we should get back an object which describes Jane in our system. Our test is speaking about intent, not just confirming value equality. Notice that the type of the object used for comparison doesn't matter, and this holds for properties too.

Note that I omitted the test for null in the second variant. You don't need it because the deep equality tester will deal with that just fine. However, you are obviously still free to include it for the sake of clarity.

NExpect gets this "for free" by depending on a git submodule of PeanutButter and importing only the bits it needs. In this way, I can re-use well-tested code and consumers don't have to depend on another Nuget package. Seems like a win to me.

What if we didn't care about all of the properties? What if we only cared about, for example, Name and Id. A dead Jane is still a Jane, right?

NExpect has you covered:

  [Test]
  public void IntersectionEqualityTesting()
  {
    // Arrange
    var sut = new PersonRepository();
    
    // Act
    var person = sut.FindById(1);

    // Assert
    Expect(person).To.Intersection.Equal(new {
      Id = 1,
      Name = "Jane"
    });
  }


We can also test the type of a returned object:

  [Test]
  public void TypeTesting()
  {
    // Arrange
    var sut = new PersonRepository();

    // Act
    var person = sut.FindById(1);

    // Assert
    Expect(person).To.Be.An.Instance.Of<Person>();
    Expect(person).To.Be.An.Instance.Of<IPerson>();
    Expect(person).To.Be.An.Instance.Of<BaseEntity>();
  }

We can test for the exact type (Person), implemented interfaces (IPerson) and base types (BaseEntity).

Next, we'll explore simple collection testing. Tune in for Level 2!

No comments:

Post a Comment

#PeanutButter.Utils: the dictionaries Dictionaries, HashMaps, whatever you want to call them -- they can be one of the most useful constru...