Freigeben über


Unit Testing a WCF RIA DomainService: Part 1, The IDomainServiceFactory

I’ve always been a proponent of driving product quality through unit testing. Regardless of the specific testing methodology, I enjoy the confidence a rich suite of tests can bring to product and application development. For WCF RIA developers, I wanted to start a three-part series on how to test the business logic that resides at the core of your application; your DomainService operations. During this series, I’ll identify patterns, practices, and tools that make testing your DomainServices simple (and maybe even fun?).

The first step to making your DomainService testable is to identify the external dependencies.

A DomainService with Dependencies

We’ll start by looking at a service that has both business logic and dependencies. I’ve authored it in a pretty straightforward manner so I can highlight specific changes that will improve testability.

   public class BookClubDomainService :
    LinqToEntitiesDomainService<BookClubEntities>
  {
    private readonly LibraryService _libraryService = new LibraryService();
    private readonly ApprovalSystem _approvalSystem = new ApprovalSystem();

    // Test 1: Operation should return all books
    // Test 2: Operation should return books with categories
    // Test 3: Operation should return books ordered by BookID
    public IQueryable<Book> GetBooks()
    {
      return this.ObjectContext.Books.Include("Category").
               OrderBy(b => b.BookID);
    }

    // Test 1: Operation should insert book
    // Test 2: Operation should set the added date
    // Test 3: Operation should request approval for book with invalid ASINs
    // Test 4: Operation should request approval for book not yet published
    public void InsertBook(Book book)
    {
      if ((book.EntityState != EntityState.Detached))
      {
        this.ObjectContext.ObjectStateManager.
          ChangeObjectState(book, EntityState.Added);
      }
      else
      {
        this.ObjectContext.Books.AddObject(book);
      }

      book.AddedDate = DateTime.UtcNow;

      if (!this._libraryService.IsAsinValid(book.ASIN))
      {
        this._approvalSystem.
          RequestApproval(book.Author, book.Title, book.PublishDate);
      }
      else if (book.PublishDate > book.AddedDate)
      {
        this._approvalSystem.RequestApproval(book.ASIN);
      }
    }
  }

A quick look at this service should reveal three concrete dependencies. First, we’re using an Entity Framework ObjectContext to talk to a database. We also have a dependency on an external service, LibraryService, and an internal subsystem, ApprovalSystem. The ObjectContext is a unique case which I’ll tackle in the next post, so for now let’s look at what we can do with the LibraryService and ApprovalSystem.

There are a few options available for testing code with external dependencies. The first is to actually test against the real components; perhaps with modified connection strings to test instances. For example, you could run your tests against a test instance of the database. The second is to use a mocking framework to provide mock implementations for the methods you are using. For example, you could write a quick implementation of LibraryService.IsAsinValid to use only with your tests. I’ve been using Moles recently, but there are many good frameworks available. Finally, you can use a pattern called Dependency Injection to allow your service code to be passed references to external dependencies.

The rest of this post will cover how to use the IDomainServiceFactory interface to enable Dependency Injection. There’s plenty to be said about the other testing options I listed above, but I won’t do it here. A little research should be able to help you determine if and when either of the other options is right for you.

Factoring External Dependencies

Now that we’ve identified our external dependencies, we can change the design slightly to allow them to be pass in to the DomainService. Instead of using the concrete types, LibraryService and ApprovalSystem, we’ll replace them with interfaces.

   public interface ILibraryService
  {
    bool IsAsinValid(string asin);
  }
   public interface IApprovalSystem
  {
    void RequestApproval(string author, string title, DateTime publishDate);
    void RequestApproval(string asin);
  }

Next, we’ll update the DomianService to use these interfaces. More importantly, we’ll update the service to require these dependencies be provided to the constructor.

   public class BookClubDomainService :
    LinqToEntitiesDomainService<BookClubEntities>
  {
    private readonly ILibraryService _libraryService;
    private readonly IApprovalSystem _approvalSystem;

    public BookClubDomainService(
      ILibraryService libraryService, IApprovalSystem approvalSystem)
    {
      if (libraryService == null)
      {
        throw new ArgumentNullException("libraryService");
      }
      if (approvalSystem == null)
      {
        throw new ArgumentNullException("approvalSystem");
      }

      this._libraryService = libraryService;
      this._approvalSystem = approvalSystem;
    }

    // Test 1: Operation should return all books
    // Test 2: Operation should return books with categories
    // Test 3: Operation should return books ordered by BookID
    public IQueryable<Book> GetBooks()
    {
      return this.ObjectContext.Books.Include("Category").
               OrderBy(b => b.BookID);
    }

    // Test 1: Operation should insert book
    // Test 2: Operation should set the added date
    // Test 3: Operation should request approval for book with invalid ASINs
    // Test 4: Operation should request approval for book not yet published
    public void InsertBook(Book book)
    {
      if ((book.EntityState != EntityState.Detached))
      {
        this.ObjectContext.ObjectStateManager.
          ChangeObjectState(book, EntityState.Added);
      }
      else
      {
        this.ObjectContext.Books.AddObject(book);
      }

      book.AddedDate = DateTime.UtcNow;

      if (!this._libraryService.IsAsinValid(book.ASIN))
      {
        this._approvalSystem.
          RequestApproval(book.Author, book.Title, book.PublishDate);
      }
      else if (book.PublishDate > book.AddedDate)
      {
        this._approvalSystem.RequestApproval(book.ASIN);
      }
    }
  }

If you tried to run this service now, you’d see it failing with an error along the lines of “No default constructor exists for the type BookClubDomainService”. To fix this error, we’ll need to understand a little more about how DomainServices are instantiated.

IDomainServiceFactory

Each time a client calls into the DomainService endpoint, the RIA hosting layer uses the singleton DomainService.Factory to create a new instance of the requested DomainService type. The default IDomainServiceFactory implementation expects a DomainService type to provide a parameter-less constructor. Since we’ve changed the constructor above to require two parameters, we now have to create a factory type that can handle our DomainService.

Factories are pretty straightforward to write and only expose a pair of Create/Release methods.

   public class BookClubDomainServiceFactory : IDomainServiceFactory
  {
    public DomainService CreateDomainService(
      Type domainServiceType, DomainServiceContext context)
    {
      DomainService domainService;
      if (typeof(BookClubDomainService) == domainServiceType)
      {
        domainService = new BookClubDomainService(
          new LibraryService(), new ApprovalSystem());
      }
      else
      {
        domainService = (DomainService)
          Activator.CreateInstance(domainServiceType);
      }

      domainService.Initialize(context);
      return domainService;
    }

    public void ReleaseDomainService(DomainService domainService)
    {
      domainService.Dispose();
    }
  }

Now that we have a custom factory, we’ll have to make sure it gets used to instantiate our DomainService. The easiest way to do this is to add a Global.asax file to our web site and bootstrap the factory there.

image   image

   public class Global : System.Web.HttpApplication
  {
    protected void Application_Start(object sender, EventArgs e)
    {
      DomainService.Factory = new BookClubDomainServiceFactory();
    }
  }

Now we can run our service successfully again. The update we made to the DomainService will allow us to pass in test-specific implementations of the external dependencies. This in-turn improves the isolation and consistency of our tests. In parts two and three, I will show how to treat your data access layer as an external dependency and how to actually write the tests.

[A Note on Dependency Injection]

Often Dependency Injection is done in a more generic fashion using Injection frameworks. If you find yourself creating a big switch in IDomainServiceFactory.CreateDomainService for each DomainService type in your custom factory, you may find it more maintainable to use a framework. There are plenty of them out there, so it shouldn’t be too hard to find one that works for you.

[An Alternate Design]

Now that you’ve patiently waded through this entire post, it’s worth noting that a custom IDomainServiceFactory is not strictly required to make your DomainService testable. For instance, you could provide two constructors for each DomainService; the parameterized constructor for testing and a default constructor that chooses a default value for each dependency. There are benefits to the factory approach (like Dependency Injection), but I’ll leave it up to you to pick the one that fits best.

Unit Testing Series

Comments