Last week, I wrote about the basics of unit testing UWP apps, including steps required to get xUnit.net and code coverage working together. This week, I’ll cover a few more unit testing topics related to testing components in isolation. That will wrap up the UWP example app that I have been building over the last few weeks.
Testing MVVM Apps
My example app uses the Model-View-ViewModel pattern, with data stored in a SQLite database accessed using Entity Framework Core. The MVVM pattern consolidates most of an app’s business logic in the view model, so it’s important to focus test effort there. But as I mentioned last week, it’s easy when testing a view model to unintentionally write integration tests rather than unit tests.
Since integration tests exercise an app the same way the View does, they are useful in providing end-to-end test coverage. However, they have three problems compared to true unit tests:
- They run more slowly;
- They require more of the app to be written; and
- They are more likely to break as a result of code changes.
While there are ways to solve all of these problems at once, SQLite also provides an option to solve just the first one, using in-memory databases. This solution gives you the advantages of integration testing plus the speed of unit testing.
SQLite In-Memory Databases
Setting the database filename
One of the steps in using a SQLite database in an app is to specify the database filename. If you use the special filename :memory:, then the database will be created in memory rather than as a disk file. Since memory is much faster than a disk, even a solid-state disk, this means your tests will run much faster. Furthermore, you can create multiple independent in-memory databases by using multiple database connections. This is helpful for isolating tests from each other to avoid cross-test interference.
An in-memory database isn’t practical for real users of your app, since all of their data would disappear whenever the app closed. So you need database connection code that specifies a filename on disk. But for unit tests, you want a way to indicate the special :memory: filename. There are a few ways to use both types of databases in the same data access layer. For my example app, I used the approach recommended by the official Microsoft documentation on Testing with SQLite and Entity Framework Core. Here’s a summary:
An app that uses Entity Framework for its data access layer needs to implement a class that inherits from DbContext
, the EF class that represents a database session. In the example app, I used the class name TaskContext
. That class overrides a method called OnConfiguring
, which gets called on each database access, and can specify a SQLite filename by calling UseSqlite
on DbContextOptionsBuilder
.
Before calling UseSqlite
in TaskContext
, I call IsConfigured
to find out if the database filename has already been set. If it has, it means that either a unit test has already set it to :memory:, or a previous call has already set it to a filename. In either case, I can skip the configuration step. In this way, an on-disk database is used when the data layer is being used by an app, and an in-memory database is used by unit tests.
Integration testing with the in-memory database
Integration tests as defined above require two common setup steps, which I extracted into private methods in the example app’s test class:
GetConnection
: Create, open, and return aSqliteConnection
with theDataSource=:memory:
connection string. A connection is created at the start of each test, and is closed in afinally
block at the end of the test. This process isolates tests from each other, even if they run in parallel, and it starts each test with an empty database.GetContext
Using the connection, create and return aTaskContext
. Each section of the test uses a different context, but because the contexts share the same connection, they also share the same in-memory database.
The first step in each test is to call context.Database.Migrate
, which tells Entity Framework to apply the database schema to the empty in-memory database.
Each subsequent step starts by creating a Repository
, the part of the data access layer that uses a TaskContext
instance to call the database via Entity Framework. I modified Repository
to accept TaskContext
in its constructor, so that callers can create a repository for either an in-memory or an on-disk database.
With that infrastructure in place, here are some scenarios we can test:
- Load a task from an empty database, and verify that no exceptions are thrown.
- Create and save a task. In a new context, load the task and verify that it has the correct name.
- Create and save a task. In a second context, load the task, update its name, and save it. In a third context, load it again and verify that it has the new name.
Since these tests use a real database, they test code paths that are very close to those used by the View when the app is running.
Real Unit Tests
Although integration tests with an in-memory database are easy to write and run quickly, they aren’t always the best choice. If a MVVM integration test fails, the fault could lie either in the view model or the data access layer. And if the data access layer hasn’t yet been written, you can’t even write this type of integration test. To isolate and test just the view model, we need a different kind of test.
Fakes, Mocks, Stubs, and Dummies
In a real application with an object-oriented design, most objects depend on other objects or components. In the example app we’re discussing, TaskViewModel
depends on Repository
, which depends on TaskContext
, which depends on Entity Framework and SQLite.
If our goal is to verify the behavior of just TaskViewModel
, then we would like to avoid having to incidentally verify the behavior of all of its dependencies. A standard way to do that is to replace the actual dependencies with simplified versions that have known, fixed behavior.
There are a few different types of test objects that can be used to accomplish this goal. Here is one list, as defined by test automation expert Gerard Meszaros and quoted by Martin Fowler, an early unit testing proponent:
- “Dummy objects are passed around but never actually used.”
- “Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production.” Fowler (or Meszaros) gives the example of an in-memory database, so by this definition the tests discussed above were using a “fake data access layer.”
- “Stubs provide canned answers to calls made during the test.”
- Mocks are “objects pre-programmed with expectations which form a specification of the calls they are expected to receive.”
In my experience, mocks are the most useful for testing real apps. I’ll be using them to test my time tracker. But for my example app, I’ll stick with a simple stub object, RepositoryStub
.
Interface-based programming
If you want to be able to replace a dependency with a test double (one of the objects defined in the list above), it helps if your classes depends on interfaces rather than other concrete classes.
I mentioned above that the TaskViewModel
constructor accepts a Repository
instance, so that tests can pass in a repository that is associated with an in-memory database. That works fine when we want to test against a real database (just one that is faster than normal). But what if we don’t want to depend at all on Entity Framework? We just want to test our view model. To do that, it helps if our constructor accepts not a Repository
, but an IRepository
.
IRepository
is an interface that defines all of the public members of Repository
without implementing them. In the unit test project, we can then create a class called RepositoryStub
that implements IRepository
without referencing a database. In my example, the LoadTask
method returns a task with a constant name value. And the SaveTask
method does nothing.
Given this stub version of the repository, we can now write a unit test that exclusively tests TaskViewModel
. For example, it can verify that after loading a task, the task name is set correctly. We could write this test even if we hadn’t yet written any of the data access layer code.
Next Steps
That’s it for the UWP example app. You can find the code for this week on GitHub at UWP-MVVM-EF-SQLite-4. It was a useful research project that helped break in the UWP, C#, XAML, MVVM, EF Core, SQLite, and xUnit.net stack that I’ll be using for my project. But starting next week, I’ll be working on the real app. I’m sure that as I go along I’ll find more interesting problems and solutions, which I’ll document here.