This week, we're going to be looking at
integration tests for Web API applications. And so the application I've got here is actually the
one I was using last week when we're talking about Swagger, which is just a simple RESTful API using
Web API. So we've got an entity of BookReview with an Id, a Title and a Rating. We've got an SQLite
database containing a few BookReviews. We've got a DbContext using Entity Framework to access that.
And then we've got a repository interface with the various methods we need to access those reviews
and also an SQLite implementation of that. And when we run this up, we can see that we get the
Swagger interface that we can use to interact with that. So I can go to, for example, the GET and
execute that and we see the list of BookReviews that we've got in there. And so we've got those
for all of the operations. I've got some GETs, a PUT, a POST and a DELETE, all of which
we've got defined in the controller here as pretty straightforward methods accessing that
data. And when we're developing - and when we're testing - having Swagger can be very useful,
because it means you can take a quick look, see that everything's working okay. But in terms
of more serious testing, it's very much as we see it here, manual testing: having to open up
the section, click 'Execute', and so on and so forth. What we'd like is some kind of automated
tests that can go through that as well. Now, there are tools available with Swagger that will
do automated scripted tests for you. Similarly, there are tools with Postman and lots of
other ones as well. But they have the slight disadvantage that they're not part of your C#
.NET project, you've got to learn a separate set of skills and keep them in a separate place in
order to use those tests. And also, they're not really quite integration tests. They’re actually
end-to-end tests, because they're going to test your API all the way through to the data in the
database. And that always introduces problems, because when you've got data in a database, if
the data is wrong, then you'll get errors even if your actual functionality is right. And so we also
would like to be able to mock the data so we know what data we're testing against. And so that's
why typically we go for unit testing. And so I've actually written some unit tests here. So here
we've got a unit testing library written in xUnit. There we've got the BookReviewsControllerTests,
and we can see what we've got here. So I'm following quite a common convention that
people have with tests here of having three sections in the test name: so the operation we're
going to test, the circumstances under which it's going to work and then the results we get back. So
we're saying that if we do get, it always returns all the books, and we're using the Moq library
to create just a couple of books - doesn't matter about the details. And we use that to mock
the repository. But then here, we've got the actual code that does the testing. So we create a
controller programmatically - this is one of the key things about unit tests that you just write
C# code - and we're creating a controller there and passing in the mock repository. So normally
in the controller we would be injecting whatever has been configured to implement that interface.
But we're not worrying about injection here, we are just passing in our mock object. We then just
call the method that we're looking at - so the Get in this case. And then we verify that we've got
the right type of results - an OkObjectResult in this case - and also verify that the data that's
come back matches the data expected. And then we've got the GetbyId_IfExists_ReturnsBook. Again,
some mock data. Again, we create the controller, call the method directly, this time passing
in ‘2’ to get the book with Id number 2, and then check everything's okay. And then also, we
need to test error conditions. So the next test, GetById_IfMissing_Returns404. And so we've only
got books 1 and 2 here, but we asked for book number 3, and we should get the NotFoundResult.
And then we've got a few more tests or other things - doesn't matter too much what's going on
in there. So those are unit tests. And obviously, if we run those tests, then we're going to get
green lights because everything's working in that case. And so that looks pretty good. But we are
not testing quite the same functionality as we are when we go through Swagger or something like that,
because we're directly accessing these things programmatically, as we are there, and therefore
anything to do with the infrastructure around that - like the routing, or the serialisation and
deserialization of the JSON - things like that aren't going to be caught if we get them wrong in
these unit tests. And I can show you a couple of examples of that. Let's go back to the controller
and let's suppose on there I made a mistake that on this Get by Id, you can see there I've declared
the format of the route that is going to pass in ‘id’ and there I'm passing an ‘id’ as the actual
parameter. But if I got that wrong - so now the name there doesn't match up with the name there -
well, if I run the tests, then we still get green lights, because all the test is doing is calling
that function. It doesn't bother to look at what's happening in the definition of the route. Whereas
if I run that in Swagger … and so there we've got our get one. Now we can see it's got really
confused because it's now seeing two parameters, one called ‘id’ one called ‘ix’. The id it
thinks is part of the query string and the ix it thinks is part of the path. If we try
that one out - so if I ask for number 3, and execute that - weirdly, we get a not found.
And that's because, if we go back to the code, the id is the thing that's actually being used,
not the ix. And the id, which was part of the query string, we left it blank, so it defaults to
zero. And so actually, it's always trying to look up element zero. And so that passed the unit tests,
but actually had a bug in it that we didn't spot. Another problem that we can get in the other
direction, is the fact that our unit tests are actually locking the implementation of this
controller into a particular way of doing things, which remember unit tests should never do;
we should be simply testing the output, not how the implementation works. But if
you take a look at the test for the Post, then you can see that we're assuming that
the Post returns a CreatedAtActionResult; that is the correct behaviour for the test.
And that's okay, because in the post here we are returning a CreatedAtAction. But actually,
that doesn't really matter. That CreatedAtAction is how we form up the return value. But there's
other ways of doing it as well. For example, rather than CreatedAtAction - which basically
allows you to provide the name of the action method in the controller, and forms up the
response from that - we could use an alternative, which is CreatedAtRoute. And that doesn't
form up the name of the URL itself, you actually pass them as a string, which in
this case is just going to be a blank string, because we're not using named endpoints,
we're just using the verb like Get. So if I just put that in there, then behaviourally that
is exactly the same. So if we just run up Swagger and exercise it in there we go to the Post, we’ll
try it out, we'll create a new review for ‘Emma’ and give that a rating of ‘4’ and then execute
that. And we can see, we still get the 201 coming back; we've still got the new Id of 28. So
that's working fine. But if we run the unit tests, that now fails because we returned a
CreatedAtRootResult, not a CreatedAtActionResult. And so although the behaviour is correct,
it's complaining about the way we did it. And so that's not allowed. So that's causing us a
number of problems. And so what we'd like to do is get the best of both worlds: have automated tests
that we can write in C# but which go through the whole cycle of checking the routes as well as
the functionality of the controller. So let's see how we do that. And what we need to do is
we can add here a new project - we could do as part of the same project, but I'm going to do a
new project. So we'll add this and this can use any testing framework you like. So I'm going to
stick with xUnit, but it would work with NUnit or whatever you go for. And we'll call this same
as the overall project plus ‘.IntegrationTests’. And then first thing we obviously have to do
is reference the classes we’re testing, so we'll put that in there. And then what I
like to do is mimic the folder structure in the project under test in my test code. So I'll
add in here a new folder called 'ControllerTests'. And then I can get rid of
those two. And then in here, we'll add our tests of the BookReviewsController.
So BookReviewsControllerTests. But then to make these integration tests, I've got to start doing
a little bit of extra work. So what I'm going to do is also in here, I'm going to add a class
which I'm calling CustomWebApplicationFactory. And it's called that because it needs to be
derived from a class called WebApplicationFactory. And that actually comes in from a different
package, so we'll just get that automatically installed. And so that will give us that. And then
this is a generic and the class you want to pass in as the generic parameter is actually the class
which configures your web application. So in our case, that is the ‘program.cs’, which is where
we do all the configuration of our DbContext, our injection, our pipeline, all the things that
we're going to need to use to some extent or other in part of the integration tests. So that's
got to be there. Problem we have there though: if we try to just type in ‘Program’, we can see
what it thinks we're after is this thing called ‘MicrosoftVisualStudio.TestPlatform.TestHost.Program’,
which is completely the wrong thing. And it looks like it's very difficult to get
hold of the Program we've got here. And the reason for that is that we have generated
this project using the approach that doesn't give us the full Program class with the Main
method. And so from the code we've got here, it's actually generating the Program class, but
it's generating it as internal. And that means it's not visible in our test code over here. So
what I have to do, if I just inside that program, if I right click on it and go to the quick actions
you can see we can convert to a Program.Main style of program. And if I do that, it just generates
that code. But then we can see that's what the problem was that this Program was being declared
as internal. But now we've got access to the code, we can just make that ‘public’. And so
now in here, that Program is the Program that we want. So that's got that working.
Then having done that, let's copy that, and go back to our actual tests. So in
here, we're going to make use of that application factory. And the way we do it
is this. We're going to declare a private field of that type that we'll just call ‘_factory’
and then we're also going to declare a ‘private HttpClient’ called ‘_client’. And
then we'll have a constructor, which creates both of those. The factory is
easy to create, we just say ‘_factory = new’ all of that. And then we say ‘_client’ then
we don't say ‘= new HttpClient()’, we say ‘= _factory.CreateClient()’. And that's what gives us
this kind of fake HTTP Client, not connecting to a genuine web service, but just connecting to our
web service in the test environment. Now something we've got to watch out for: both of those - the
HttpClient and the factory - are disposable. And so we've got to dispose of them. So we need to
make the test class itself IDisposable and then implement the interface and then in the
Dispose just call Dispose on each one of those, like that. And bear in mind, the way that xUnit
works: for each and every test, we will create and then dispose of the class. So this will be
done once on each test; we're going to have a brand new factory, a brand new client for each
test, which is what we want. Having done that, we can now start writing actual tests.
And let's actually just copy them over from the original unit tests, because basically
we want to test the same things. So let's go and take the first one of these, which was the one
that just checks that we always get the books. So we'll grab that and go back in here. And
that will be our first test. But we've got to make a few changes. Well firstly, we've got to get
hold of Moq, so that's easy enough. But actually, let's keep things simple for now. Let's
not even worry about the mock data. And so let's not compare against anything, let's
just check that we've got the appropriate result. But this is where things get a bit different,
because we're no longer going to directly make the call to the controller, we're going to go
through the HttpClient that we just created. And so what I'm going to say here is ‘var response
= _client.GetAsync’. Now it’s an async function, so we're going to have to put an ‘await’ on it
there, which means we're also going to have to change this to an ‘async Task’. And then on that
GetAsync we merely need to put the path to where we want to go, which is ‘/BookReviews’ because
that was the name of the controller without the word ‘Controller’. So that's what we have there.
And so that's replaced those two calls of creating the controller and calling the appropriate
method, that's just going to map straight on there. And then the last thing we want to do
is just verify that the response code is the OK status. And so all we need to do for that
is an ‘Assert.Equal’ and then ‘HttpStatusCode - it can't find that so we've got to do another
‘using’ - and then ‘.OK’ and we check that is the ‘response.StatusCode’. And
so now if we run those tests, we actually get an error and the error we'll see
there is basically that it can't find the database file. So actually, we're going to have to do some
mocking, before we can get this to work at all. So this is how we do the mocking. We go back to
this factory and we're going to have to do a couple of things in here. We're going to declare
a public property, which is going to be a Mock of our repository. So that's ‘IReviewRepository’. And
we'll call it ‘Mock’ and we'll make it get-only. And then obviously, we've got to create that in
the constructor. So in the constructor we will say that ‘= new’ all of that, so just normal
Moq behaviour. But then the thing that's a bit different: inside this Web Application
Factory, we're going to override a method called ‘ConfigureWebHost’. And this is where we
can override the default dependency injection configuration that it picked up from the Program
here and we can provide our own, because we want to use this mock object. So what I'm now going
to do is say ‘builder.ConfigureTestServices’. And that gives us a lambda, and in there, I'm
going to say, 'services.AddSingleton' and then 'reviewRepositoryMock.Object'. So that means, whenever someone asks for the IReviewRepository, they'll get that mock object. You may think it's a bit odd we're doing
this as a singleton, because more normally - say, here - we'd do the repository as Scoped, but actually, it doesn't
matter here because, remember, this thing is being recreated each and every time because of the way
that xUnit runs that around. So, having done that, even though we haven't configured that mock for
any actual behaviour, that should just be enough now that it will be there when it's needed,
and we should get the green light just saying that we got back the status code of OK,
even though we didn't actually get any books. But now we can go a step further
and now we can start doing our mocking. So, we'll uncomment those mock books. But now,
rather than creating our own repository locally, we're going to use the '_factory.' and then we've got that
public property for the repository. And so there, we're passing in the mockReviews, which means we can now
also have a look at the collection there, except that it's not 'result.Value', we've got
to do one more thing: we've got to unpack that data as it came out of the response.
So, we'll say something like 'var data =' and then we'll do a 'JsonConvert.DeserializeObject' and we'll do that
to an IEnumerable of BookReviews, rather than an array. And then we do that on 'await response.Content.
ReadAsStringAsync()' So that will unpack the data from the body of the response. And, again, that's something we want
to do to make sure that our JSON formatting is working correctly. Then finally, we just need to pop
data in there, and that should all be okay. So now, as well as simply testing that the status is
correct, we're also going to be getting the correct result. And once we've done that, we can now
move on and do that for the other tests as well. So, if we go back to our unit tests, rather than
our integration tests, let's grab the next one. So this one is GetById_IfExists_ReturnsBook, and
the process of conversion starts to become fairly mechanical. So we still create our mocks.
We don't create our own local mock repository; we just put it into the one we've created in the
factory. We don't create our controller; we call 'client.GetAsync', which, of course, means
again, we've got to make this an 'async Task'. On this occasion, because we're passing in the Id,
we're going to put a '/2' on there. We don't need any of that. We again want to check that our response is giving
us okay and again, we want to extract the data. But, in this case, we want to extract
the data just to a single BookReview, and then we can just put 'data' in there. And if we run
those tests, we should get the second one working. And then, if we just very quickly do all the remaining
ones, so that was the one with the missing, the one for the summary and the one for the Post. So, we'll
copy those, go back in here and it becomes really quite straightforward, as I said. So, we need
to change that to 'async Task'; we need to change that code there. Let's grab those. And on
this one, we've just got to remember we're asking for number 3. And then, we want to
make sure that that corresponds to 'NotFound'. And actually, I think, once you've got the hang of
this, these integration tests are actually less code to write them than the unit tests because you
don't have to worry about the various typecastings and that sort of thing. It's all much smoother.
Here, we've got the summary. So again, let's cheat that a little bit. So there's our mocking
going in there. And then our call is going to be to 'GetAsync("/summary")’. Again, we've got to make it an 'async
Task'. And then we want to make sure the status code is OK once again. So let's steal that from up here, because we're
going to unpack the data in the same sort of way. The data will be an IEnumerable of BookReviews.
So we'll pop that in there. And then once again it's going to be the 'data' that we're doing the tests on. And the finally for the post: 'async Task' ... the mock data is much simpler, but we still want to change that to a ‘_factory.ViewRepositoryMock’. Notice what we're testing for here is both that
the Create method on the repository is called, and that the SaveChanges is called. So we are
making both of those verifiable, and then we verify them down there. Then we are going to make our HTTP
request. But this time, of course, it's going to be a PostAsync. And then because it's a Post, we need to provide
a body. So there is a handy little class called JSONContent - just get from the namespace - and that has a
method called Create, which will take our object and turn that into JSON and put it in the body,
so that gets sent off. So that's quite a nice thing we can do there. And then all we've got to really do there is assert
that we get the appropriate status code, which was going to be be ‘Created’. And then we do the 'VerifyAll' to make sure that everythingwas called correctly. So with a little bit of luck, all of those tests should now pass, and they do, so that's working fine. But those should be more
robust against the problems I was talking about. So remember the couple of problems that we had.
One was, if we go back to the controller, if we change that to 'ix', if we now run the tests, then we get a failure along the lines of what we got when we tested that manually in Swagger, but that was missed by the unit tests, and you can see, still is being missed by the unit tests. So that one is now caught, but then the other problem we had, which was the
other way round, was on the Post. So if we changed that from a ‘CreatedAtAction’ to a ‘CreatedAtRoute’
and change that just to blank, that gave us an error in the unit tests, which it didn't give us
an error in Swagger. And we want the integration tests to reflect Swagger, and that's what's
happening. So in both those cases, integration tests seem to be better than unit tests because
they catch the sort of errors that unit tests miss. So that's what you can do, you can obviously fill
that out a bit and add further tests for all of the interactions. But what I've done here is actually probably not particularly
smart, and you wouldn't see this done in the real world, in that I've done both unit tests and integration
tests testing exactly the same thing, which is duplication of code. And given that I think I've persuaded you the
integration tests are better, and also, once you've got the infrastructure in place,
slightly easier to write, I would typically just be writing the integration tests and only maybe introduce
unit tests when you find a particular problem that for some reason isn't working with the integration tests or whatever it may be.
But integration tests like this, first, they're quite easy to use, and once you've got your web application factory
working, then they really work very smoothly. So that's quite a useful technique for automated testing of a RESTful
API. You can use these tools and it means that you're just writing C# code. You can share code. You can do your mocking
in just the same way you're used to and get your tests running automatically. So I hope you enjoyed that. If you did, do
subscribe, do click like, and I'll see you next time.