ASP.NET Integration Testing

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
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.
Info
Channel: Coding Tutorials
Views: 11,803
Rating: undefined out of 5
Keywords: c#, ASP.NET, Swagger, Unit Testing, Integration Testing, Captioned
Id: Hmp2ctGacIo
Channel Id: undefined
Length: 25min 6sec (1506 seconds)
Published: Fri Mar 31 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.