Unit Testing a WCF RIA DomainService: Part 3, The DomainServiceTestHost

In this thrilling conclusion to my series on unit testing, I’ll show you how to use the DomainServiceTestHost to test your DomainServices. In parts one and two I showed you how to extract external dependencies using the IDomainServiceFactory and how to use the Repository Pattern. Now that all the groundwork is out of the way, I’ll show you how to test your business logic.

The DomainService

The DomainService we’re testing looks like this.

   public class BookClubDomainService : RepositoryDomainService
  {
    public BookClubDomainService(
      IUnitOfWork unitOfWork,
      IBookRepository bookRepository,
      ILibraryService libraryService,
      IApprovalSystem 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() { ... }

    // Test 1: Operation should return all books for category
    // Test 2: Operation should return books ordered by BookID
    public IQueryable<Book> GetBooksForCategory(int categoryId) { ... }

    // 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) { ... }

    // Test 1: Operation should update book
    // Test 2: Operation should return validation errors
    public void UpdateBook(Book book) { ... }

    // Test 1: Operation should update book
    // Test 2: Operation should update the added date
    [Update(UsingCustomMethod = true)]
    public void AddNewEdition(Book book) { ... }

    // Test 1: Operation should delete book
    // Test 2: Operation should require authentication
    [RequiresAuthentication]
    public void DeleteBook(Book book) { ... }

    // Test 1: Operation should return the most recent added date
    public DateTime GetLatestActivity() { ... }
  }

It has the standard Query, Insert, Update, and Delete operations in addition to custom Query, Update, and Invoke(/Service) operations. The constructor accepts a number of parameters; each representing an external dependency in the code. I’ve labeled each method with the tests that we’ll write against it so we don’t have to know the details of the implementation (they’re in the sample, I’m just skipping them in the post).

The DomainServiceTestHost

The DomainServiceTestHost is a new class in the Microsoft.ServiceModel.DomainServices.Server.UnitTesting assembly (now on NuGet and soon to be shipping with the Toolkit). It’s designed to help you test individual DomainService operations. The API falls closely in-line with DomainService conventions (and may help clarify them if you’re still a little hazy). For instance, the test host has a Query method for retrieving data, and Insert, Update, and Delete methods for modifying it.

In addition to standard operation support, the test host makes it simple to test validation and authorization. For each standard signature in the DomainServiceTestHost, there is a TryXx variant that makes it easy to capture validation errors. For instance, Query is paired with TryQuery and Insert with TryInsert. Also, each time you create a test host, you can pass an  IPrincipal into the constructor that the operations should be run with. This makes it easy to cycle through a number of test users as you validate your authorization metadata. Finally, the test host can be created with a factory method that it uses to instantiate a DomainService. This allows you to initialize a DomainService with test-specific dependencies.

Testing a DomainService

Finally we’re getting to the good part. In this section, I’ll walk you through the steps necessary to unit test your business logic. First, we’ll initialize the local variables we’ll use with each test.

   [TestInitialize]
  public void TestInitialize()
  {
    this._libraryService = new MockLibraryService();
    this._approvalSystem = new FakeApprovalSystem();
    this._unitOfWork = new FakeUnitOfWork();
    this._bookRepository = new MockBookRepository();

    this._domainServiceTestHost =
      new DomainServiceTestHost<BookClubDomainService>(
        this.CreateDomainService);
  }

  private BookClubDomainService CreateDomainService()
  {
    return new BookClubDomainService(
             this._unitOfWork,
             this._bookRepository,
             this._libraryService,
             this._approvalSystem);
  }

As you can see, I’ve written simple mock/fake/stub types for each dependency. I discussed the MockBookRepository a little in my previous post. For the context of the following tests, it’s important to point out that I’ve initialized the repository with an initial set of data. The other three are just simple test implementations. Also, I’ve provided a CreateDomainService method that I can pass to the test host that initializes my BookClubDomainService with the test dependencies. If you’re not familiar with the Visual Studio testing, the [TestInitialize] method will get called before the start of each test.

Starting with something simple, we’ll take a look at the test for the default query.

   [TestMethod]
  [Description("Tests that the GetBooks query returns all the books")]
  public void GetBooks_ReturnsAllBooks()
  {
    IEnumerable<Book> books = this._domainServiceTestHost.
                                Query(ds => ds.GetBooks());

    Assert.AreEqual(
      this._bookRepository.GetBooksWithCategories().Count(), books.Count(),
      "Operation should return all books");
  }

In this method, we’re asking the test host to return the results of the query. The ‘ds’ parameter in the lambda is the instance of the DomainService we’re testing. Since the IntelliSense for the query operation is a little verbose (as is anything where the Expression type shows up), I’ll give you another sample so you get the hang of it. This time we’re testing the custom query.

   [TestMethod]
  [Description(
     "Tests that the GetBooksForCategory query orders books by BookID")]
  public void GetBooksForCategory_OrderedByBookID()
  {
    int categoryId = this._bookRepository.GetTable<Category>().
                       First().CategoryID;

    IEnumerable<Book> books = this._domainServiceTestHost.Query(
                                ds => ds.GetBooksForCategory(categoryId));

    Assert.IsTrue(books.OrderBy(b => b.BookID).SequenceEqual(books),
      "Operation should return books ordered by BookID");
  }

In this snippet we’re passing a local variable to the query operation, but everything else is pretty much the same. We’ve gotten the returned collection and now we’re verifying that it is in sorted order.

Tests for Insert, Update, and Delete operations are just as easy to write. Just calling the method on the host will redirect to the corresponding operation in your DomainService.

   [TestMethod]
  [Description("Tests that the InsertBook operation inserts a new book")]
  public void InsertBook_InsertsNewBook()
  {
    int categoryId = this._bookRepository.GetTable<Category>().
                       First().CategoryID;

    Book book = new Book
    {
      ASIN = "1234567890",
      Author = "Author",
      CategoryID = categoryId,
      Description = "Description",
      PublishDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)),
      Title = "Title",
    };

    this._domainServiceTestHost.Insert(book);

    Assert.IsTrue(book.BookID > 0,
      "New book should have a valid BookID");

    Book addedBook = this._bookRepository.GetEntities().
                       Single(b => b.BookID == book.BookID);

    Assert.IsNotNull(addedBook,
      "Operation should insert book");
  }

To rephrase what I said above, calling Insert on the test host with a Book calls into our DomainService operation, InsertBook. As you can see, at the end of this test our new book has been added to the repository.

In addition to Query, Insert, Update, and Delete methods, Named Updates and Invoke operations are also supported. In both cases, the syntax is very similar to testing a Query.

   this._domainServiceTestHost.Update(
    ds => ds.AddNewEdition(book), original);
   DateTime result = this._domainServiceTestHost.Invoke(
    ds => ds.GetLatestActivity());

AddNewEdition updates the book and runs custom business logic and GetLatestActivity returns a DateTime related to the newest book. Again, the ‘ds’ parameter in the lambda expression refers to the DomainService being tested.

Testing Validation and Authorization

Testing validation and authorization metadata for a DomainService operation can be just as important as testing the business logic. A unit test suite verifying authorization and validation would be a great tool to prevent service security regressions.

Validation is fairly straightforward to test. Instead of using the test host methods I’ve already described, you should use their TryXx variants.

   [TestMethod]
  [Description("Tests that the UpdateBook operation returns 
     validation errors when passed an invalid book")]
  public void UpdateBook_SetsValidationErrors()
  {
    Book original = this._bookRepository.GetEntities().First();
    Book book = new Book
    {
      AddedDate = original.AddedDate,
      ASIN = "Invalid!",
      Author = original.Author,
      BookID = original.BookID,
      Category = original.Category,
      CategoryID = original.CategoryID,
      Description = original.Description,
      PublishDate = original.PublishDate,
      Title = original.Title,
    };

    IList<ValidationResult> validationErrors;
    bool success = this._domainServiceTestHost.TryUpdate(
                     book, original, out validationErrors);

    Assert.IsFalse(success,
      "Operation should have validation errors");
    Assert.AreEqual(1, validationErrors.Count,
      "Operation should return validation errors");
    Assert.IsTrue(validationErrors[0].MemberNames.Single() == "ASIN",
      "Operation should return a validation error for 'ASIN'");
  }

This test calls into UpdateBook with invalid data and then verifies the resulting validation errors are ones we expect.

Authorization tests follow a different approach. Instead of using a different test host method, they set a custom principal that will be used when calling the DomainService operation. While the test host defaults to an anonymous user, an alternate constructor allows you to specify the user that is interesting for your test case.

   this._domainServiceTestHost = 
    new DomainServiceTestHost<BookClubDomainService>(
      this.CreateDomainService,
      BookClubDomainServiceTest.authenticatedUser);

Conclusion

To sum it all up, the DomainServiceTestHost in combination with an IDomainServiceFactory and the Repository pattern makes it simple to unit test your DomainServices in an isolated and reliable fashion. Also, since the test host is just a .NET type, it should be compatible with any and all test tools and frameworks you feel like using it with. Hopefully this series gave you a good overview of DomainService unit testing. If you have any questions or feature requests, please send them my way. Thanks.

[A Note on the DomainServiceTestHost]

As of the initial release of this test host, there may still be a few edge cases for association and composition change sets that are not supported. If you find any, please let me know so I can put together a scenario for a subsequent release.

Unit Testing Series