Boy Meets 'Hello World'

Blogging the journey from College Grad to .NET Developer

  Home  |   Contact  |   Syndication    |   Login
  11 Posts | 0 Stories | 5 Comments | 0 Trackbacks

News

Archives

Post Categories

Wednesday, April 23, 2008 #

I often have found that working with small, immutable objects can be very helpful. First off, they are extremely easy to test, typically not needing any sort of mocking involved. Second, due to their immutable state, you can easily override Equals to use to your advantage. The advantage I'll talk about today is the ability to introduce a new test seam that doesn't depend upon inversion of control to be able to test an object.

 

Suppose I'm writing an object that will represent a query. The query should return all blogs that contain certain text in their titles...

 

public class BlogWithTextInTitle : Query<Blog, BlogCollection>
{
    private string text;

    public BlogWithTextInTitle(string text)
    {
        this.text = text.ToLower();
    }

    public BlogCollection RunOn(IQueryable<Blog> queryable)
    {
        var results = from b in queryable
              where b.Title.ToLower().Contains(text)
              select b;
        return new BlogCollection(results.ToArray());
    }
}

 

Fun stuff! Now, suppose that I am going to write something in my service layer that will query all the blogs with the text "Hello World" in the title, and send a warning to their owner about how unimaginative their blog titles are.

(Stupid examples. I'm full of 'em.)

Now, you'll notice this query implements an interface, Query. My repository will have something like a "Find" or "RunQuery" method on it to run these queries (check out Ayende's post "Thinking about repositories" for more details on this). The service might look like this...

 

public class SendWarningService : ISendWarningService
{
    private Repository repository;

    public SendWarningService(Repository repository)
    {
        this.repository = repository;
    }

    public void SendWarningToThoseWithTextInTitle(string text, string warningMessage)
    {
        var query = null; // How do I create the query, while still being able to use it in a test?
            
        BlogCollection blogs = repository.FindAll(query);
        blogs.SendWarningToAll(warningMessage);
    }
}

 

I need to test that the blog collection returned has it's SendWarningToAll method called, but how can I control what FindAll returns? Typically, I would use a stub, but I would need to create that stub in the test method. How could I then pass that stub to the actual method? I could put a QueryFactory into the constructor, and then mock out the factory to have it return my stub, but I don't like that idea. Interestingly, we don't seem to have this problem with value types, do we? Let's assume that the query were a value type (which, I guess if you really wanted to, you could do by making it a struct, but I don't think I'm comfortable with that). Here's what the test would look like...

 

   1:  public void GivesWarningToBloggersReturnedFromQuery()
   2:  {
   3:      // Create test values.
   4:      var text = "Hello World";
   5:      var warningMessage = "Hello World? 'cmon, it's been done before!";
   6:   
   7:      var expectedQuery = new BlogWithTextInTitle(text); // Check it out, I'll just go ahead and create this thing...
   8:   
   9:      var repository = new FakeRepository();
  10:      var blogCollection = new FakeBlogCollection();
  11:   
  12:      // Set the repository to reutrn the fake blog collection when it recieves the query object.
  13:      repository.SetupResultFor(expectedQuery).Return(blogCollection);
  14:   
  15:      // Exercise the test.
  16:      var service = new WarningBlogsService(repository);
  17:      service.SendWarningToAllWithTitleContaining(text);
  18:   
  19:      // Check that the collection had it's message sent.
  20:      Assert.That(blogCollection.MessageWasSent(warningMessage), "Message was not sent.");
  21:  }

 

Notice that at no time am I ever passing the query object (expectedQuery) into the service. Instead, the service will be allowed to make another query object, simply using the new operator.

 

   1:  public class SendWarningService : ISendWarningService
   2:  {
   3:      private IRepository repository;
   4:   
   5:      public SendWarningService(IRepository repository)
   6:      {
   7:          this.repository = repository;
   8:      }
   9:   
  10:      public void SendWarningToThoseWithTextInTitle(string text, string warningMessage)
  11:      {
  12:          var query = new BlogWithTextInTitle(text);
  13:          
  14:          BlogCollection blogs = repository.FindAll(query);
  15:          blogs.SendWarningToAll(warningMessage);
  16:      }
  17:  }

 

See on line 12, I just go ahead and create the object. However, if I ran the test as it is, it would fail, because right now my fake repository (and any mock framework you use) will probably be doing the default testing of equality by reference. Obviously, the object created in my test is not equal to the one created in the actual method, because they do not reference the same object. However, by overriding Equals on our query, we can actually make this work...

 

public class BlogWithTextInTitle : Query<Blog, BlogCollection> {
    private string text;

    public BlogWithTextInTitle(string text)
    {
        this.text = text.ToLower();
    }

    public BlogCollection RunOn(IQueryable<Blog> queryable)
    {
        var results = from b in queryable
              where b.Title.ToLower().Contains(text)
              select b;
        return new BlogCollection(results.ToArray());
    }

    public override bool Equals(object obj)
    {
        var other = obj as BlogWithTextInTitle;
        if (ReferenceEquals(null, other)) return false;

        return text.Equals(other.text);
    }

    public override int GetHashCode()
    {
        return text.GetHashCode();
    }
}
 

This is sort of like using a whole new test seam. Typical test seams used are the constructors, properties, and method parameters, which are all used to pass mocks and stubs. The problem is that you can start filling these up quickly. By using value equality to your advantage, you can "pass" a stub into your system under test without using another parameter.

Now I've been bitten before by screwing up such a repetitive thing like overriding Equals. Furthermore, Testing that you've overridden Equals correctly could include a bunch of tests...

 

  • Not equal to null
  • Not equal to an object of a different type.
  • Not equal to an object of the same type with different parameters.
  • Equal to an object of the same type with same parameters.

 

That's four tests to test three lines of code (I don't test GetHashCode()... technically you could just return 0 and things would still work, just with a performance hit, but do read the comments below!). And, I've found that I do this a lot. For example, testing that you threw a custom assertion correctly is much easier when all you need to do is create the assertion yourself and check that this assertion equals the one just thrown.

So, eventually, I started needing to do these kind of tests enough that I created my own class for it. Basically, the test will test that a class adheres to the idea of Constructor Equality. This means that two objects will be considered equal if they were created by using the same constructor parameters.

 

   1:  public class ConstructorEqualityTest<T>
   2:  {
   3:      private readonly T same1;
   4:      private readonly T same2;
   5:      private readonly T[] different;
   6:   
   7:      public ConstructorEqualityTest(T same1, T same2, params T[] different)
   8:      {
   9:          this.same1 = same1;
  10:          this.same2 = same2;
  11:          this.different = different;
  12:      }
  13:   
  14:      public IList<string> Failures
  15:      {
  16:          get
  17:          {
  18:              IList<string> failures = new List<string>();
  19:                  
  20:              if (!PassesEqualToNullTest())
  21:              {
  22:                  failures.Add("Type should not be equal to null.");
  23:              }
  24:   
  25:              if (!PassesEqualToOtherTypeTest())
  26:              {
  27:                  failures.Add("Type should not be equal to a different type.");
  28:              }
  29:   
  30:              if (!PassesEqualToSameConstructorArgs())
  31:              {
  32:                  failures.Add("Type should be equal to another type constructed with the same arguments.");
  33:              }
  34:   
  35:              foreach (int indexMarkedAsSame in GetDifferentObjectsMarkedAsSame())
  36:              {
  37:                  failures.Add("Type should not be equal to another type constructed with different arguments (Different Argument " + indexMarkedAsSame + " was seen as same)");
  38:              }
  39:                  
  40:              return failures;
  41:          }
  42:      }
  43:   
  44:      public string FailureMessage
  45:      {
  46:          get
  47:          {
  48:              StringBuilder stringBuilder = new StringBuilder();
  49:              stringBuilder.AppendLine("The object did not pass all requirements for Constructor Equality.");
  50:   
  51:              foreach (string failureMessage in Failures)
  52:              {
  53:                  stringBuilder.AppendLine(failureMessage);
  54:              }
  55:   
  56:              return stringBuilder.ToString();
  57:          }
  58:      }
  59:   
  60:      private bool PassesEqualToSameConstructorArgs()
  61:      {
  62:          return same1.Equals(same2);
  63:      }
  64:   
  65:      private bool PassesEqualToNullTest()
  66:      {
  67:          return !same1.Equals(null);
  68:      }
  69:   
  70:      private bool PassesEqualToOtherTypeTest()
  71:      {
  72:          return !same1.Equals("A_String");
  73:      }
  74:   
  75:      private IList<int> GetDifferentObjectsMarkedAsSame()
  76:      {
  77:          IList<int> list = new List<int>();
  78:          for (int index = 0; index < different.Length; index++)
  79:          {
  80:              if (same1.Equals(different[index]))
  81:              {
  82:                  list.Add(index);
  83:              }
  84:          }
  85:   
  86:          return list;
  87:      }
  88:          
  89:      private bool PassesEqualToDifferentConstructorArgs()
  90:      {
  91:          return GetDifferentObjectsMarkedAsSame().Count == 0;
  92:      }
  93:   
  94:      public bool Passes()
  95:      {
  96:          return PassesEqualToNullTest() &&
  97:              PassesEqualToOtherTypeTest() &&
  98:              PassesEqualToDifferentConstructorArgs() &&
  99:              PassesEqualToSameConstructorArgs();
 100:      }
 101:  }

So now, if I wanted to test that my query works adheres to the concept of "Constructor Equality", I could use this test...

 

[Test]
public void AdheresToConstructorEquality()
{
    var same = new BlogWithTextInTitle("same");
    var same2 = new BlogWithTextInTitle("same");
    var different = new BlogWithTextInTitle("different");

    var test = new ConstructorEqualityTest<BlogWithTextInTitle>(same, same2, different);

    Assert.That(test.Passes(), test.FailureMessage);
}
 

First, I created two objects that should be equal. I'll also created additional different objects that will all be tested to ensure that Equals will return false. Each one should be different from the "same" objects from one parameter. If I had a class that had two parameters, I would use the following...

 

[Test]
public void AdheresToConstructorEquality()
{
    var same = new MyClassToTest(1 ,2);
    var same2 = new MyClassToTest(1, 2);
    var different1 = new MyClassToTest(-999, 2);
    var different2 = new MyClassToTest(1, -999);

    var test = new ConstructorEqualityTest<MyClassToTest>(same, same2, different1, different2);

    Assert.That(test.Passes(), test.FailureMessage);
}
 

My test will fail to begin with, with the failure message telling me that the objects created with the same arguments should be equal, but they aren't. Then I would override equals until my test passes.