Freigeben über


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

Comments

  • Anonymous
    August 23, 2011
    My team and I are immensely thankful that you posted this unit testing series. We've been struggling with how to make our domain services more testable, and this series is perfect! Is there any chance you could do a post or series about the client-side version as well, whether that's putting the domain context behind a repository pattern or otherwise?  I would really appreciate it! There are some existing posts out there on the subject, but the approaches seem a bit scattered, and some of them feel dated, and I'm hoping you could at least say what the current best options are for client-side unit testing (at least in the specific case of RIA services domain context and, if it matters, the domain service being linq-to-entities). Much like your RepositoryDomainService and DomainServiceTestHost, it seems like there's a bunch of common things that could be provided to help ease the development of unit tests on the client side as well. :) Thanks again, Kyle!  You rock!

  • Anonymous
    August 23, 2011
    @James I considered doing one for Silverlight, but most of the guidance falls along the lines of MVVM. Your 'business logic' falls in your View Model and you mock out the 'Service' layer. I'd recommend starting with this post. blogs.msdn.com/.../mvvm-pattern-for-ria-services.aspx

  • Anonymous
    August 24, 2011
    Kyle I created a mock RIA EF domain service class which inherits from the "real" EF Domain Service class a few weeks ago; it's a slightly different approach which ends up in a similar place except you don't have the TestHost.... have a look when you have five minutes... cockneycoder.wordpress.com/.../creating-a-testable-wcf-ria-domain-service-part-2. Cheers

  • Anonymous
    August 25, 2011
    @Issac It's actually much closer to a take on the Repository Pattern (see Part 2) and is a bit orthogonal to the Test Host. What it misses (and what the test host provides) is a way to test your code in the RIA Services pipeline without spinning up an actual hosting environment and calling the service endpoints. Testing your business logic outside of the pipeline lowers the fidelity and usefulness of your tests.

  • Anonymous
    September 13, 2011
    Thanks for your posts they are always very educational! Did you ever think about writing a book about mvvm, design patterns, prism, unity or mef using dependency injection. Do you have any favorite books that you could reccommend?  

  • Anonymous
    September 20, 2011
    @William I don't have any particular recommendations. I always start at a page like silverlight.net and go from there.

  • Anonymous
    November 09, 2011
    I am researching using WCF RIA Services for some upcoming work and was concerned about testing. This is an excellent series of articles. There appears to be a lot of modifications to the DomainService as generated by the Domain Service Wizard - I am concerned that whenever our database/model changes, does this mean we cannot use the wizard or have to manually update the generated files?

  • Anonymous
    November 11, 2011
    @Alan When you use the repository pattern, it's best not to use the wizard. There's enough that's significantly different that it would just be a waste of your time. That pattern's not a necessity for testing, though. It's just a common approach to abstracting your tests from a database. As an alternative approach, you could use standard generated code and just make sure to point it at a test database. There are a number of options that I outlined in this series, but there's nothing forcing you to use them in any specific combination.

  • Anonymous
    February 26, 2012
    Hi Kyle, This is an excellent post which successfully consolidates concepts/patterns that are loosely explain elsewhere with reference to WCFRIA. I modified the source so that the repository got data from a real db, via the Entity Framerwork, in essence returning 'real' book entities. This was done by refactoring the method (public MockBookRepository()), and essentially allowed me to switch to 'real' entities once I have successfully tested with mock entities. This is great as it allows a staged development whereby I can move to real entities once i am happy that everything works with test data. What I am having problems with is getting the update/insert/delete to work in the same way, i.e. adjuting the update so this updates the entities in the entity framework, as opposed to the mock in house repositor. This would make the frameowkr great as could do all CRUD testing with mock data and then switch to the real data context (ef and database) once testing was successfull. Can you please advise as to how this can be done please?

  • Anonymous
    February 27, 2012
    @Simon I think I addressed your concern about how to use repositories in this post (blogs.msdn.com/.../unit-testing-a-wcf-ria-domainservice-part-2-the-repository-pattern.aspx). It shows both test and production-focused implementations. Also, the full source for this sample is available on code.msdn (code.msdn.microsoft.com/Unit-Testing-a-WCF-RIA-a39a700e).

  • Anonymous
    March 01, 2012
    Hi Kyle, Yes indeed it does thanks I just added a new reposity for EF and injected that instead of your mock repository, and all good Thanks again

  • Anonymous
    June 13, 2012
    Hi Kyle, I use composition and can't get it to work with the DomainServiceTestHost. When you say 'there may still be a few edge cases for association and composition change sets that are not supported', do you mean composition is not supported at all or only certain edge cases of composition? Say I have the following Update method:        public void UpdateLogbook(Logbook currentLogbook)        {            var originalLogbook = this.ChangeSet.GetOriginal(currentLogbook);            if (originalLogbook == null)            {                this.Context.Logbooks.Attach(currentLogbook);            }            else            {                this.Context.AttachAsModified(currentLogbook, originalLogbook);            }            foreach (var logbookEntry in                this.ChangeSet.GetAssociatedChanges(currentLogbook, logbook => logbook.LogbookEntries))            {                var operation = this.ChangeSet.GetChangeOperation(logbookEntry);                switch (operation)                {                    case ChangeOperation.Insert:                        this.Context.AttachAsAdded(logbookEntry);                        break;                    case ChangeOperation.Update:                        this.Context.AttachAsModified(logbookEntry, this.ChangeSet.GetOriginal(logbookEntry));                        break;                    case ChangeOperation.Delete:                        this.Context.AttachAsDeleted(logbookEntry);                        break;                }            }        } How can I use the DomainServiceTestHost to test adding a new LogbookEntry to the Logbook.LogbookEntries collection? The following test method, which I expected not to work, throws an exception.        [TestMethod]        public void GivenNewLogbookEntryWhenInsertingThenShouldAddLogbookEntry()        {            var logbookEntry = new LogbookEntry();            LogbookEntry addedLogbookEntry = null;            this.domainContextMock.Setup(t => t.AttachAsAdded(It.IsAny<LogbookEntry>())).Callback<LogbookEntry>(t => addedLogbookEntry = t);            this.domainServiceTestHost.Insert(logbookEntry);            Assert.AreSame(logbookEntry, addedLogbookEntry);        } I suspect I'll need to manually create a ChangeSet somehow and submit that. Could you provide some guidance on that please?

  • Anonymous
    June 14, 2012
    @Remco Composition is one of those scenarios that didn't make the initial cut. Unfortunately I don't have sample lying around of the verbose way to test. Essentially you'll have to call DomainService.Submit with a ChangeSet. To do that you need to do a number of things. (1) Write an IServiceProvider (2) Create a DomainServiceContext (3) Create a DomainService (4) Call DomainService.Initialize (5) Call DomainServiceDescription.GetDescription (6) Create a ChangeSetEntry for each parent and child entity (7) Create a ChangeSet containing those entries (8) Call DomainService.Submit Sorry for the inconvenience, but I hope this starts you in the right direction.

  • Anonymous
    June 18, 2012
    Thanks Kyle. I got it working. I was a bit confused at first about the ChangeSetEntry class Id property. I first thought this was the primary key of the entity in the changset entry, but if I understand it correctly these id's are generated on the client for each new changeset to allow associations in a changeset entry to reference the changeset that is the other side of the association, correct? It will also specify an order I assume in which the entries in a changeset need to be processed on the server, correct?