Unit Testing Basics for Windows Store Apps - Programming Windows Store Apps with C# (2014)

Programming Windows Store Apps with C# (2014)

Appendix B. Unit Testing Basics for Windows Store Apps

I’m being very careful to call this section “Unit Testing Basics,” emphasis on the “Basics.” Describing unit testing is beyond the scope of this book, and my objective here is to provide a basic outline of unit testing for total newbies. Specifically, I’m looking to show how the integrated unit-testing features work in Visual Studio 2012, and finally demonstrate that what we’ve done in the book is unit-testable.

We’ve gone to great lengths in our project to use MVVM—a very common objective of which is to provide unit-testing vectors. The question is, does it work?

Unit Testing for Newbies

If you’re not using unit testing in your day-to-day work, you should be. Unit testing is like having a savings account and the spare cash and discipline to put a little aside each month. There’s a little upfront pain, but when you need it, you’ll be extremely glad it’s there.

The principle of unit testing is based on the truism of software engineering that “system in known state plus known value yields predictable result.” So, if you have a method that multiplies two values together, you can create a unit test that plugs in two known values and checks against a known result. The principle is that if you happen to break that method somewhere down the line, you’ll be told about it before it ends up in the customer’s hands.

So a typical test might look like this:

// test the results of multiply...

private void TestMultiply()

{

// make sure that the Multiply things method returns an expected result...

Assert.AreEqual(77, MyMagicClass.Multiply(7, 11));

}

That’s a contrived example, but the principle behind it is sound. What you’re really looking to do is build up a set of functions that are able to go through and exercise all of the individual parts of the app. There are all sorts of benefits to unit testing, such as easier refactoring, and having the tests operate as a type of documentation. Another strong advantage—which is a big thing I like about it—is that if you build your tests first you can get all of the sides of a problem expressed in test “stubs” before you start coding. For example, if you are writing tests to check the functionality of code that emails invoices to customers, you can go ahead and block out tests for “what happens if there’s no email address,” “what happens if there are no invoices,” “what happens if the email address is there, but invalid,” and so on. This “test first” approach makes development easier as well as making your code more likely to pass acceptance testing first time.

Anyway, it’s not my intention here to teach you everything about unit testing or why it’s a good thing. If you’re new to it, all you need to know to follow along is that we’re going to produce test functions that are able to exercise different aspects of the code that we’ve built so far.

Creating a Test Project

Creating a test project is much as you would imagine: simply add a new Windows Store Unit Test Library project into the solution. Figure B-1 illustrates.

Adding a new Unit Test Library project

Figure B-1. Adding a new Unit Test Library project

You’ll need to modify the references for the test project to include the UI-agnostic project.

Testing RegisterServiceProxy

The reason why we built all of the MVVM stuff in this book was so we could do things like decouple the view-model and service proxies from the actual service implementation when we were undertaking unit testing.

This concept applies equally well to databases as it does to web services. The principle behind decoupling comes back to the central premise of unit testing—that is, “system in known state plus known value yields predictable result.”

For example, if the remote service is unavailable when your tests are running, all of your tests that rely on it being available will fail. That situation would create a whole swath of invalid test results. Moreover, this assumes that the service’s persistent store is in a known state when you start—for example, you might try to register a user with a given username that happens to work the first time, as the username hasn’t been used, but would fail on subsequent attempts. (This point also applies to local databases or file stores—having a persistent state between runs can really muck up unit testing, or at least make it difficult to revert to a known state on start.)

The way to get around this is to create a simulated implementation of the service and call that simulation rather than the real thing.

The easiest thing to do is create a “fake” implementation. This involves building a new object that implements whatever interface or interfaces the originally consumed object did. You can then make that object return whatever you fancy depending on what you are testing. Faking is the method that we’re going to use here.

In .NET, it is common to create mock objects. Usually this involves using a library; a popular one to do this with .NET is called Moq. However, for security reasons, Windows Store apps do not support the technology that enables Moq to emit new assemblies and consume them usingSystem.Reflection.Emit. Thus, for all intents and purposes, mocking is currently impossible in Windows Store apps.

To get started, the first thing to do is create a class called FakeRegisterServiceProxy. This will implement IRegisterServiceProxy, but its implementation of the RegisterAsync method will not call the server. The following code simply returns a GUID if the supplied username is mbrit; otherwise, it’ll return an error. Note that we have to obtain a Task to return to the caller.

public class FakeRegisterServiceProxy : IRegisterServiceProxy

{

public Task<RegisterResult> RegisterAsync(string username, string email,

string password,

string confirm)

{

if (username == "mbrit")

return Task.FromResult<RegisterResult>(new RegisterResult

(Guid.NewGuid().ToString()));

else

{

var error = new RegisterResult(new ErrorBucket());

error.AddError("Invalid username.");

return Task.FromResult<RegisterResult>(error);

}

}

}

There are two more things that we need to do in order to make this functional. We need to get TinyIoC to return the fake implementation rather than the real one, and we need to get the unit tests to handle async methods.

Starting the Runtime and Handling async Methods

We already have a method for starting the runtime—namely, the Start method in StreetFooRuntime. We need to make sure this is called whenever our test is initialized.

Visual Studio will create a unit-test class called UnitTest1 for us when we create a new project. Rename this class as RegisterServiceProxyTests and then add a Setup method to get the test started. Decorating this with the TestInitialize attribute ensures that Visual Studio’s test runner will confirm that this method completes before executing any discovered tests.

Visual Studio’s test runner has support for asynchronous test and setup methods. As per our discussion on asynchronous methods in Chapter 2, we have to give the unit-test runner a chance of understanding that our method will run asynchronously. As you might expect, we can do this by marking the method as async and having it return a Task instance.

As part of the setup, we need to override the default behavior of the TinyIoC’s automatic registration such that when we ask for a handler for IRegisterServiceProxy, we get our faked implementation back. We do this by calling the SetHandler method on the ServiceProxyRuntimesingleton. Here’s the code:

[TestClass]

public class RegisterServiceProxyTests

{

[TestInitialize]

public async Task Setup()

{

await StreetFooRuntime.Start("Tests");

// set...

ServiceProxyFactory.Current.SetHandler(

typeof(IRegisterServiceProxy),

typeof(FakeRegisterServiceProxy));

}

}

Next we need to actually test the method. As this method uses asynchronous methods, we again have to mark it as async and have it return a Task instance. It’s the Task instance that makes this all magically work—it tells the Visual Studio test runner that it has to handle the asynchrony.

// Add method to RegisterServiceProxyTests...

[TestMethod]

public async Task TestRegisterOk()

{

var proxy = ServiceProxyFactory.Current.GetHandler

<IRegisterServiceProxy>();

// ok...

var result = await proxy.RegisterAsync("mbrit",

"matt@amxmobile.com", "Password1", "Password1");

Assert.IsFalse(result.HasErrors);

}

To run the tests, select Test Run All Tests from the menu. The Test Explorer will open and you’ll see the results of your tests. Figure B-2 illustrates.

A successful test run

Figure B-2. A successful test run

Testing the View-Models

That’s the basics of the unit testing covered. The view-models need a little bit more work, as these are dependent on having a IViewModelHost to use. Luckily, this was built with unit testing in mind, so all we need to do is to create a fake implementation of that too.

The Visual Studio test runner can run tests in parallel, so we can’t use a static singleton for this class. We’ll have to create a new instance of it for each test. This is easily done. The functionality that I’m proposing putting into this example will just keep track of how many messages have been displayed. With invalid data, we’ll validate that we got an error. With valid data, we’ll validate that we did not. We’ll have to capture the message shown, as we’ll need to do specific validation on this to determine whether the operation worked or not. In a production implementation, you’d want to be more nuanced than this.

Here’s the code for FakeViewModelHost. For this illustration, I’ve left some of the methods as throwing a “not implemented exception”:

internal class FakeViewModelHost : IViewModelHost

{

public int NumMessagesShown { get; private set; }

public string LastMessage { get; private set; }

internal FakeViewModelHost()

{

}

public IAsyncOperation<IUICommand> ShowAlertAsync(ErrorBucket errors)

{

return ShowAlertAsync(errors.GetErrorsAsString());

}

public IAsyncOperation<IUICommand> ShowAlertAsync(string message)

{

// update the number of messages...

this.NumMessagesShown++;

this.LastMessage = message;

// return...

return Task.FromResult<IUICommand>(null).AsAsyncOperation

<IUICommand>();

}

public void ShowView(Type viewModelInterfaceType, object args = null)

{

throw new NotImplementedException("This operation has not been

implemented.");

}

public void ShowAppBar()

{

throw new NotImplementedException("This operation has not been

implemented.");

}

public void HideAppBar()

{

throw new NotImplementedException("This operation has not been

implemented.");

}

public void GoBack()

{

throw new NotImplementedException("This operation has not been

implemented.");

}

}

Finally, here are our two tests:

[TestClass]

public class RegisterPageViewModelTests

{

[TestInitialize]

public async Task Setup()

{

await RegisterServiceProxyTests.SharedSetup();

}

[TestMethod]

public void TestRegisterCommandWithInvalidPasswords()

{

var host = new FakeViewModelHost();

var model = ViewModelFactory.Current.GetHandler

<IRegisterPageViewModel>(host);

// set...

model.Username = "mbrit";

model.Email = "mbrit@mbrit.com";

model.Password = "foobar";

model.Confirm = "barfoo";

// check...

Assert.AreEqual(0, host.NumMessagesShown);

// run - this will fail validation on the password...

model.RegisterCommand.Execute(null);

// check...

Assert.IsFalse(host.LastMessage.Contains("The new user has been

created."));

}

[TestMethod]

public void TestRegisterCommandWithOk()

{

var host = new FakeViewModelHost();

var model = ViewModelFactory.Current.GetHandler

<IRegisterPageViewModel>(host);

// set...

model.Username = "mbrit";

model.Email = "mbrit@mbrit.com";

model.Password = "F00bar";

model.Confirm = "F00bar";

// check...

Assert.AreEqual(0, host.NumMessagesShown);

// run - this will fail validation on the password...

model.RegisterCommand.Execute(null);

// check...

Assert.IsTrue(host.LastMessage.Contains("The new user has been

created.")); }

}

And that’s it. Now if we run our tests, all three will pass (as illustrated in Figure B-3).

Our successful unit tests

Figure B-3. Our successful unit tests