Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Let's Get Implicit With Our Tests

0.00/5 (No votes)
20 Jan 2017 1  
Using the implicit operator and fluent APIs to simplify building tests

Introduction

So, I was talking to our very own Christian Graus last night about one of the coolest, but most underused features available to us in .NET right now. We were talking about the use of the implicit operator. Now, for those who don't know what the implicit operator is, this is what MSDN has to say about this:

"The implicit keyword is used to declare an implicit user-defined type conversion operator. Use it to enable implicit conversions between a user-defined type and another type, if the conversion is guaranteed not to cause a loss of data."

While we were talking about this, I happened to mention that I like to use implicit in unit tests to allow me to easily build an object with mocked behavior. I then added that I like to use fluent APIs to allow me to customize tests as needed, without having to write much repetitive code. In this tip, we're going to walk though creating a simple set of tests that demonstrates the implicit operator.

The Interfaces and Implementing Them

Because I am a big fan of using interfaces and mock objects in tests, these are the interfaces we are going to create; in the grand tradition of programming manuals just about everywhere, we're going to call them IFoo and IBar.

public interface IFoo
{
  bool ShouldAssert { get; set; }
  void DoSomething();
  IBar Bar { get; }
}

public interface IBar
{
}

As we can see, these are incredibly trivial interfaces. Now, we're going to implement IFoo in a Foo class (I love our clever naming conventions).

public class Foo : IFoo
{
  private readonly IBar bar;
  public Foo(IBar bar)
  {
    this.bar = bar;
  }
  public bool ShouldAssert { get; set; }
  public void DoSomething()
  {
    if (ShouldAssert)
    {
      throw new InvalidOperationException();
    }
  }

  public IBar Bar => bar;
}

Again, nothing too major here. The only thing of any real note (apart from the fact we're using C# 6 syntax for the Bar setter), is that setting ShouldAssert to true will trigger an exception.

The Tests

We're going to use Visual Studio Tests with Moq here but feel free to substitute it with whichever test and mock frameworks you like. The underlying principles will be the same regardless.

Now, we're going to have three tests (not an exhaustive list of tests for Foo I know, but handy for demonstrating the concepts). Each test will use a new Foo instance to demonstrate the behaviors. In order to create a new instance of Foo each time, we would do something like this:

IBar bar = new Mock<IBar>().Object;
Foo foo = new Foo(bar);

That's not too much code but imagine how much we would have to do if Foo had a complex constructor, injecting many interfaces. Then imagine that we wanted to set the expectations of behaviors on those interfaces. Again, suppose that we need to test Foo in multiple test classes, the code repetition is too great. Now, we could create a helper extension that is one place to build this object for us. Well, by introducing implicit, we can do just that. Let's start off with our FooBuilder class.

public class FooBuilder
{
  private IBar bar;
  private bool shouldAssert;

  public FooBuilder()
  {
    bar = new Mock<IBar>().Object;
  }
  public FooBuilder WithBar(IBar bar)
  {
    this.bar = bar;
    return this;
  }

  public FooBuilder WithShouldAssert(bool shouldAssert)
  {
    this.shouldAssert = shouldAssert;
    return this;
  }

  public static implicit operator Foo(FooBuilder fooBuilder)
  {
    return new Foo(fooBuilder.bar) { ShouldAssert = fooBuilder.shouldAssert };
  }
}

Let's walk through this class. We create a default boolean for shouldAssert because we don't want to default to throwing an exception when we call DoSomething in our Foo class. We also provide a default mocked version of IBar so we don't need to take care of this elsewhere if we don't need to - this would allow us to set up any expectations of behavior on IBar that we wanted to have readily available. The real magic happens in the implicit operator - this static method will return us a new instance of Foo whenever we instantiate this class. Because this is implicit, there is no need to cast this back to Foo - that's taken care for us "automagically". Now, one thing we have done here is provide the ability to override the default values if we need to. This is particularly handy when we want to do something that goes beyond the defaults. By using a Fluent API, we don't create the returning instance until we have finished the setup chain. This is very handy when we want to build arbitrarily complex behavior later on.

So, what do our tests look like?

[TestClass]
public class FooTests
{
  [TestMethod]
  public void TestWithDefaultBuilder()
  {
    Foo foo = new FooBuilder();
    Assert.IsFalse(foo.ShouldAssert);
  }

  [TestMethod, ExpectedException(typeof(InvalidOperationException))]
  public void ThrowsAssertion()
  {
    Foo foo = new FooBuilder().WithShouldAssert(true);
    foo.DoSomething();
  }

  [TestMethod]
  public void NewBar()
  {
    IBar bar = new Mock>IBar<().Object;
    Foo foo = new FooBuilder().WithBar(bar);
    Assert.AreEqual(bar, foo.Bar);
  }
}

As we can see, we aren't casting from FooBuilder back to Foo - as far as the code is returned, we are returning a Foo type here. Yes, it is possible to return different types using implicit and following the same pattern to build them, but we don't need to do that in our tests so we aren't going to do so.

So, next time you're working on a complex system that has lots of complex interactions in your classes, have a think about whether or not this little tip could save you some heartache, grief and repetitive typing.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here