Building a .NET 6 API Using TDD

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in this video we're going to build a dot net 6 web api from scratch using test driven development [Music] hey everyone wes here with productive dev i hope you're doing well it's been a little while since i've actually built a new.net project from scratch and so i thought it would be a great time to build a new web api using.net 6 and test driven development if you've watched my channel in the past you know that i'm a big fan of tdd specifically as a tool for learning how to write unit tests and learning how to write testable code if you're unfamiliar with tdd really the main concept is that we're going to write unit tests for our code before we actually write the code itself and so there are a few steps to tdd and basically what we're going to do is we're going to take a specification basically sort of like a feature request and we're going to translate that into a unit test we're actually going to write the unit test first and then we're going to actually run the unit test and see it fail once that test fails we're going to write just enough code to make that unit test pass and so we go from a red failing test to a green passing test then we're going to do some code cleanup and kind of refactor our code without changing its behavior and then basically move forward implement another test see it fail write just enough code to make that test pass or test green and then do a little refactor to kind of clean up the the code without changing its behavior and repeat this process so tdd can feel a little bit strange if you're not familiar with writing unit tests or haven't really thought to try tdd before but i think if you take the time to learn tdd and to really put it into practice a few things are going to happen first of all you're going to understand what it means to write testable code when you write a unit test for something it's really necessary to be able to test that code in isolation and so if you write your test first in a sense you're kind of forcing yourself to sort of understand how to write your code in such a way that base functionality can be tested as a unit in isolation from its dependencies so just understanding how to write code that is unit testable i think will over the long run sort of help you understand how to basically structure your code and understand the types of architectures and design decisions that basically support a really testable code base so even if you're not using tdd for you know your typical day-to-day development maybe you're writing your tests after you write your code taking tdd seriously and really sort of understanding how it works and putting it into practice is definitely going to help you over the long run really understand how to write nice modular decoupled testable code if you're interested in looking at tdd from a different language approach you can take a look at a video i made about a year ago where we used python to build a new flask api using test driven development if you enjoyed coding along with me in this video i'd really appreciate it if you gave the video a like and if you're interested in building more complex.net projects from scratch including.net 6 web apis like the one we'll be building in this video then subscribe to the channel i have a number of videos that are going to use this project as a base and we're going to look at how we can deploy it to various environments in aws so i hope you enjoy this video with that let's go ahead and dive in and get started on this project okay so let's get started and we'll create a new solution for our net web api project so to do this i'm in a projects directory and i'm actually in windows terminal and specifically i'm using git bash as a terminal emulator here i've chosen to use git bash so that we can share very similar unix-like commands across any operating system that you might be using so whether you're using a linux distro or mac os you should be able to follow along with the same commands here i'll be using git bash in a windows development environment so the first thing that we'll do here is we'll run the command.net new sln and we'll specify an output directory here we'll call cloud customers so this is going to create a new project directory for us called cloud customers that we'll move into now and you can see that in our new project directory we have a solution file called cloud customers.sln this is a net solution file and we could in fact open this up now in visual studio or in writer however it's not going to be much use to us until we create projects to associate with this solution so we're going to create two projects we'll create our web api project and we'll create our unit test project so let's go ahead and create the web api project first you can do that with the command.net new web api dash o and we'll call this cloud customers.api so this will take just a moment and we can see that what's been produced here is in fact another subdirectory called cloudcustomers.api that should be at the same level as our solution file next we'll create a unit test project and rather than web api here we're going to create this as an x unit project which is an option in the dot net cli to generate a new class library with x unit installed which is a really fantastic unit testrunnerfor.net so now we have two subdirectories these represent our two projects the web api project and the unit test project and now we can associate them with our solution using the command net sln add and here we could specify the individual relative paths to the cs proj file in each of these projects but luckily we can also use a glob pattern here to basically wildcard dot cs proj in any subdirectory and so this will grab both cs files with a single command here which is a really kind of convenient way to add projects to a new.net solution so with that let's go ahead and open up visual studio and specifically we're going to open up this cloudcustomers.sln file you could use rider it's another fantastic ide i'll be using visual studio 2022 community in this video so we'll select open a project or solution and we'll take a look at our new solution file here and so what you'll see is that in our solution explorer we have the dot api project as well as the dot unit test project both of them have some code that's been boilerplated out for us here so in the case of the unit test we have a unit test file that's been scaffolded just says unit test 1 and you see we have one unit test so with x unit you can recognize a unit test method by the fact that it has a fact attribute on it this basically tells the test runner that this method needs to be run as a unit test there's a second attribute that you'll commonly see in x unit called theory which allows us to write parameterized unit tests which is a really convenient way to write parametrized tests so you'll often see things like inline data here as a second attribute what this allows us to do is write multiple unit tests if you will or a single unit test with multiple multiple parameters and the inline data attribute will basically tell the test runner to run this particular method multiple times as many times as there are inline data attributes and each time injecting a different parameter value for the parametrized method that the attribute decorates if you will so we can also have in fact multiple parameters so let's say we had something like this this will just map to another parameter on the test method okay so very useful way to write parameterized unit tests if you just see fact as an attribute again this is just a standard parameter list unit test okay so we'll come back to our unit test here shortly note that we also have some code scaffolded out in the web api project notably we have a controllers directory that contains something called a weather forecast controller and we have a weather forecast class as well and then we have this program.cs file now what you may notice is if you're familiar and have used.net recently prior to dot net six that typically we have two files that sort of build out the entry point to our application so the program.cs is still the main entry point to our application however you may be familiar with a startup.cs file which was more commonly used prior to net 6 to basically build out the particular hosting model that we'd like to use this has kind of been consolidated in.net 6 so that you can basically do everything you need to do to build a hosting model directly within program.cs including register services for dependency injection set up our configuration and our http middleware here so just something that's been somewhat simplified for net six you can of course still use startup.cs to build out your hosting model and use it within the scope of program.cs so in other words we could do this in much the same way that we previously used net5 and previous versions of.net core but for the purposes of this particular video i'm going to stick with the new paradigm and use a singleprogram.cs file okay so conceptually what we're what we'll be building here is basically a very simple web api whose job is really to return to our clients some user data and our api itself is going to be relying on an external service accessible over http to retrieve that user data so you can think of our service as kind of a proxy where our clients are going to be making a request to us for some user data and then under the hood our web api is going to be then reaching out to some third-party customer service making http requests to that service getting some data back and then serializing it in such a way that it's useful for our clients and will return that particular customer data back as json to clients of our api so it's kind of a simple proxy to an external service where our api sort of sits in the middle sits between our clients and then some third-party customer data service okay so with that we can kind of imagine that we will have some controller that exposes an endpoint for our clients to hit it won't be the weather forecast controller but what we can do here is we can basically repurpose this to be a controller for our use case we are going to be doing tdd but we won't go so far as to to create a failing test for a non-existent controller we could definitely take that approach but in the sort of interest of a concise overview here what i'm going to do is just rename this forecast controller as a user's controller and then we'll say yes see that'll rename our class as well we're extending controller base and we're going to get rid of all of the business logic that was sort of boilerplate for us here we'll keep the logger and we'll inject the logger we'll call this getusers and for now we won't even worry about the type this will be something that we do test drive and we'll say that the task here could just be a task of i action result for now we'll make this async and for now we're going to return null so obviously there's nothing asynchronous happening in this method we just have a bare bones method that's returning null let's think about what we actually want our application to do and for that now we're actually going to start our tdd process and so we'll head over into our test project and specifically unit test one so let's kind of set things up here i'll show you how i prefer to set up small unit test projects and go ahead and create a new directory here we're going to call this systems this will be where we place all the various systems that we do test we're going to add a folder here called helpers this will be useful for creating maybe more complex mocks that we may need to use or any type of you know basic helper methods that can help us set up tests and then finally i'm going to create a folder here called fixtures which will contain standard test fixtures so maybe things like sample user data if it's useful to us and things of that nature next i'm going to add a few dependencies to our test project so just to get that out of the way we can basically manage nuget packages here and i'm going to add a very popular mocking library called mock we need to actually browse for that and we'll install this so a mock is a type of test double that essentially allows us to make assertions about the various methods that were called and with what parameters they were called on any of the dependencies of the systems that we test so we create mocks for those dependencies then we make assertions about how those dependencies were actually used essentially kind of testing the design of our software and so we can take a look at that shortly next i'm going to install a library called fluent assertions which essentially provides us with a nice syntax sort of a fluent syntax or extension method syntax for for basically making assertions and again we'll take a look at that very shortly all right so under systems let's keep this organized and we're going to have a sub directory here called controllers and then we're going to rename unit test 1 to test users controller let's say yes and then we'll move this into our controllers subdirectory and we'll treat these subdirectories as namespace providers and so for that we can change the name space to match our directory structure okay so now that everything is more or less set up we can start doing our test driven development so we kind of cheated in a sense by creating our users our users controller first we could test and we could have test driven that by basically saying hey we imagine we'd have some user's controller attempt to test it get a compile error create the user's controller call userscontroller.get realize it doesn't exist create an empty get method but again in the interest of kind of realism if you will here i've created a use user's controller we know that this is what we'd like to use and now we're going to test how we're actually going to use it using a tdd approach so we can start thinking about what it is that we actually want our system to do what's the behavior of the system that we're trying to build so i mentioned that we're making http requests on behalf of our users our users are actually making http requests to our system and our controllers are basically handling those http requests from our client so when that happens successfully we will return an http ok response or a 200 status code response so let's write a test to make that assertion we'll say that get which is a method that we'll provide on our user's controller on success returns a status code 200. okay so how are we going to basically structure our unit tests so we'll walk through this first unit test a bit more slowly just to get a feel for the structure that we'd like to use here and typically unit tests that i write take an arrange act a search structure it's really up to you as to whether or not you like to leave these types of comments in i find they can be useful so we'll call the first section arrange and this is typically where we basically set up the system under test and arrange the state of the world such that we're ready to make something happen which is what happens in the act section in this act block will typically be calling some method that we're trying to test and then in the assert block we're basically making an assertion about the outcome of that particular arrangement and action so sort of bare bones we have here as a system under test is the user's controller for this we need to actually reference our api project and so we'll right click on unit tests and we'll add a project reference to cloudcustomers.api now we can actually bring in a using for cloudcustomers.api.controllers and then we're going to take some action here which will have a result and that action in our case is to invoke the get method on the instance of a user's controller and then finally we're going to make the assertion that this returns a status code of 200 given that this was sort of happening successfully we can make that assertion like result.status code assuming that's something that exists should be two hundred [Applause] okay so we could run our tests now but we see we also have compile errors so we can fix those first of all user's controller has a dependency so let's go take a look at it it basically takes in a logger and so you know maybe in our case we're not worried about logging let's keep things simple at first we're not using a logger for now so we have a parameter list constructor here we'll head back into our test and now we see that we can new up a user's controller without any dependencies so far and we're trying to get a status code back on the result but the resolution of the action result so let's go ahead and await this and we'll make this an async task basically doesn't have our iaction result does not contain a definition for status code so there's a more specific type of i action result which does which is an ok object result so let's cast our result to an ok object result which is the type of response that we're going to get back the type of i action result for this we need to bring in microsoft asp.net core mvc and for the should we need to bring in fluent assertions and for task we need to bring in system.threading.task okay we can actually run our unit test now let's go back into the user's controller and just clean one thing up here we'll get rid of the logger we'll keep the parameter list constructor for now this class will eventually have a dependency but if we head back into our control or test rather and we want to run this unit test so one way to run the test from directly within visual studio is to view the test explorer and once the solution is built you'll see that the test explorer should discover your x unit tests and you can kind of drill down this hierarchy into the individual test level and let's just click the top level of the sort of taxonomy here and we'll hit play okay so we see a failing test which is the first step of tdd and we're getting a null reference exception when we try to execute this test it says line 12 which is the entry point to our test method so let's go back into our user's controller and see what might actually be throwing one thing we can do here is we could set a breakpoint and we can actually debug our tests if you click the drop down on the test explorer and click debug here so we're coming into the method and if we just step over of course we're returning null from the result and now we're calling result which is null dot status code so this is what's throwing our null reference so we'll continue here we see the null ref thrown and that's because our method is actually just returning null and so dot status code is going to throw the null ref all right so we can write just enough code to get this to pass well we need to return an ok object result back into our users controller what we can do is just say return okay and if we pass some result in here this will actually return an ok object result for us and so let's go ahead and try to run our unit test again so now we have a green test okay that's great of course this isn't the final sort of version or behavior of our system but it is just enough code to get our first unit test to pass when we're making a basic assertion that we should return an ok object result which itself gives us access to an http status code and it's of type 200 in this case all right but let's uh let's kind of get down to business here and and try to write a more useful test on top of this what is the sort of behavior of our system when we're trying to get users from some external service well our controller is going to need to make a subsequent request out to some third-party api and when we have real business logic like that in reality it's generally a good idea to keep that business logic you know separate and isolated from the the web layer in this case so we're going to have a sort of business logic layer that will become a dependency of our user's controller and the user's controller itself will depend on an abstraction of that service layer so let's think about how we can make that happen from a user from a tdd perspective rather we might want to make an assertion about the design of this type of system so let's go ahead and create our test method we'll say get on success this particular path basically invokes some user service which is sort of what we just described there so we'll arrange act and assert in much the same way where we have a user's controller here give us a little more screen real estate for a moment our users controller now we need to invoke some user service and so to do this our user controller is going to depend on an abstraction of that service we don't want the user controller itself to new up some new business logic service here because that would be making the user controller depend on a concretion it would be um basically not adhering to the dependency inversion principle which you you may be familiar with and so one way to sort of implement inversion of control or dependency inversion here is to use dependency injection and what that means is that we're going to provide the interface to any particular type of behavior that is going to act as a dependency for some class to the constructor of that class and then we're going to rely on a higher level scope to basically provide the implementation details to wire up that dependency that higher up scope that i'm mentioning here is effectively a dependency injection container and this is a sort of framework level with net core forward so net six included so sort of a framework level ability that we have you may also be familiar with third party packages which provide this sort of behavior or you may build your own dependency injection container at some point but packages like ninjact and structure map these are these are other types of providers you may have heard of within within.net but here we can actually wire up those dependencies directly with net itself and we can do that within program.cs and so we'll come back to this we'll basically find our way there by writing a unit test so we can imagine that our user's controller itself takes some takes some user service and we're going to create a mock for that because we don't want to actually test the behavior of this service or any sort of particular implementation of it we just want to assert that it was used in the way that we have designed here so we're going to have a variable mock user service and we're going to use that mock library to create a new mock and this is a generic type so we'll be using moq you can see that using was brought in here and so we'll have some interface i user service okay so once we have this mac user service we can provide this as a dependency to our user controller and the way that we access the underlying sort of mock implementation is to call.object and the mock library just resolves an object and mock instance of something that could implement a user service and it allows us to sort of inspect how that dependency was used in other words like what methods were called on it and we can actually set those methods up to return some control values so that we can test what happens when our dependencies behave in a certain way okay so if we're going to basically be requiring a mock user service if we were to try and actually run our unit tests here we're going to get a compile error and so we need to actually create this interface i user service now a lot of times it makes a lot of sense to create your service layer and your business logic in separate class libraries or in a separate class library for the sake of simplicity we're going to include this business logic in our api project so we're going to add a new folder here called services and what we're going to do is create a new item and for this again i need to go back to the folder view we're gonna add a new item called user service and we'll create the interface directly in the same file for now and we'll make our class user service implement that interface we'll have a parameter-less constructor for this service as well and so this should allow at least the next part of our test to compile we need a new here and now we're getting an error that user's controller actually doesn't have constructor that takes this argument so we'll go ahead and visit the user's controller which will now make depend on our i user service interface and we can create a backing field for this as well and what i'm going to do is underscore that field okay so what's happening now is that when that net itself the framework creates an instance of user's controller as it's handling http requests it's going to look at the constructor for this class and it's going to basically wire up the particular dependency that we basically tell it to when it's looking at the interface as a dependency for this class so it's saying hey i need to make a user's controller and i see that as a dependency it requires on something that implements iuser service and it's going to be our job as developers to basically instruct the framework about which particular implementation of ieser surface to create so you can see that this is really useful for cases when we're unit testing when we're basically controlling the instantiation of a user controller and we don't want to basically depend on any particular implementation of the user service we want to depend only on its expected behavior and we can control that behavior with a mock we can also inspect how that particular dependency was used by inspecting some of the features that are available on a mock like looking at what methods were called how many times and with what parameters okay so the next question is you may be wondering well how do we tell the framework that we want it to use say a particular concrete user service that implements iuser service well for that we can head into program.cs and what i'm going to do here is under builder i'm going to say configure services and we're going to pass builder.services to this method okay so services contains our service collection that the application has access to and it's basically what allows us to register dependencies for our framework level classes the top level classes in this api if you will are our controllers and so you can imagine that that net needs to build the entire dependency tree when our users controller depends on a user service the user service itself may depend on other services and so at the entry point of the application dot net's dependency injection container needs to basically resolve that entire dependency tree so what what we need to do is basically create this configure services method the type of the service collection is iservice collection and here we can say services dot add transient i user service user service there are other sort of scopes here registration scopes we have we have add singleton which is a singleton instance which is lives for the lifetime of the application we also have ad scoped which is traditionally scoped to the http request itself so it's sort of mvc.net mvc concept ad transient here is at any point we require a new instance of a user service a new instance is is created okay so we now have a way to basically register that let's head back to our test and we need to make an assertion that our user service is actually invoked as expected so how is our user service going to be used well let's write a test anticipating that sort of writing code by wishful thinking we'll say mock user service we'll call this users just to be consistent and what this mock object allows us to do is to set up methods on sorry for the typo so by calling setup we can actually set up methods on the mock and basically control the behavior of our dependencies so that it follows some particular expected path that relates to this particular test so this basically gives us access to an expression here a lambda service and what we have here is basically an instance of the service so we could call any particular method so let's imagine that we wanted to set up a method called add or get all users which doesn't exist yet but we want that to return async let's say some number of test users so we'll say for now a new list of users so we don't have a concept of a user yet we don't have a method get all test users yet but these are things that our test is sort of telling us as we're designing the system so we have a mock user service we're setting up this get all users it's going to return a list of new users let's go ahead and we could run our tests and see the compile fail but let's go ahead and actually create what we need here to get the minimal amount of code to get this passing so what we're going to do is we're going to come up into our api project i'm going to add a new directory called models and in this directory i'm going to add a new file called user.cs and we'll create a class user and let's say that per the spec that we have for this particular project our users consist of an id some name which we'll say is a string some email and then some type address okay so let's create that type and an address is basically a combination of a street a city and a zip code okay so our cloud customer user has an id a name and email and an address and an address is just basically street city and zip code for our purposes okay so if we head back into our test we can go ahead and bring in both collections generic to get the list type and then api models to get our user type and now what's missing is we don't have a method on user service specifically defined in the interface called get all users so we could generate this method so we're going to go ahead and do that and we'll go take a look at what was generated so you can see that automatically we have been given the code for the definition of get all users and then also an empty implementation in user service the class in our case i like to explicitly say public here and then what we really want to get back is a list of users wrapped in a task and so let's update the implementation here and for now it's sufficient just to throw an exception we'll head back to our test okay so we can see that what we're trying to achieve here is to assert that on a successful call our controller invokes our user service specifically get all users we can even say exactly once and so let's take action on our user controller we can say that our result now is sut dot get and we'll await that and now you'll see how we can use our mock to verify that it was actually called in the assert block so we can say mock user service dot verify service dot get all users times once okay just to make things a little bit more readable here i'm gonna do some formatting okay so you'll note that now that we've actually changed basically the way that a user controller is instantiated by passing it a dependency we also need to update our unit test that we created earlier so for that i'm just gonna use the same construction with a mock user service here object and in fact we can set up the mock the same way and let's go ahead and build and run our solution and back to solution view so we can go ahead and build the solution make sure that things are building and now we'll go ahead and run our unit tests okay so we have a failing test and the expectation is that we were to invoke the mock once but it was zero times so we didn't actually call get all users so let's write just enough code to make this unit test pass for that in our users controller what we want to do is say something like var users is our user service dot get all users and for this we can actually await that response and with that much code let's see if this actually allows our unit test to pass and it does so we have passing unit tests now and so we have another little tiny piece of business logic basically created and so let's see if we can continue writing tests to sort of assert more behavior that we would like to have implemented here clearly get all users itself is actually throwing an exception that is not implemented we head back there we can see that the real implementation of this would throw an exception so we definitely need to now take this a step further so what would we like to assert here well you're probably thinking well we would like to of course return a list of users from our user service so let's go ahead and create another method here another test method we'll say get on success returns list of users again arrange act assert and you'll notice that we're going to be duplicating some of the code that is required to basically set up a user service for test it's often useful to sort of identify you know what how these how these particular systems under tests get constructed and then we can create separate factories for them for instance to set up like happy path construction and things of that nature but for now we're going to keep things relatively simple and just for now duplicate a little bit of code here we're going to create a new instance of a mock user service and set it up and then as usual as usual here we're going to have our system under test which is our user's controller set up with that mock user service object resolved to an actual concretion and we'll say our result is again oh wait system under test.get except that we'd like to assert that this result is a type of okay object result and its value should be a you know a list of users so we could say result.should we can say b of type okay object result that's an assertion we can make and then we can say the object result itself will explicitly cast here so we have access to object result.value and we can make an assertion about that that should be of type we can actually use the generic syntax here to say list of user okay so let's go ahead and run this test you'll find that as you write unit tests in this manner in tdd things tend to get quite a bit faster over time go ahead and build again here and then rerun and be sure you rerun from the top of the the tree here okay so we failed and we see that the ex we expect the type to be a collection of cloud customer api models user but what we found was a system.string okay so we're returning a string we want to return users and that's because we're returning this all good string so we really want users to come back on the response here let's see what happens if we just plug users in for now and we hit play okay so now we have a success response so by design what we're saying here is that if we set up our user service such that it returns a new list of users in this case an empty list when we call get all users then yes everything is good because the object result we get back from the controller is going to be a new list of users however we know that if we take a look at our actual implementation when we call get all users we can head over there it's actually not implemented so even though we've set up our mock for a sort of happy path at least a path in the sense where we say hey we do return a list of users from our dependency does the system under test actually forward that list back to the user then yes we're good but what we don't have here is actually any test asserting that the service itself is behaving as expected so we can see we're testing the behavior of our controller whose business logic is very simple it's basically designed to call make a single request to a business layer a user service get all users exactly once and then return a list of those users and as designed by the interface that is definitely the case at this point so we have some passing tests but clearly we don't really have a fully functioning system as designed yet there might be some more business logic however that we're kind of ignoring here within our controller and we'll cover some of that now and i'll leave some of it to you to to think about or perhaps implement on your own and that might be like request validation what happens if we get sort of a malformed http request we get some object payload on the body or or we get some other http verb other than get but let's say that at the very least if there are no users we want to return a 404 response from this endpoint so that's something that we can definitely use tdd to generate so we'll do that now we'll say effect and in fact i'm just going to grab this test method and we'll rename it we'll say get on no users found returns 404 something like this still have a mock user service except that now we'll say that it does return an async list of users which in fact is empty and maybe that's the behavior of of our user service when no users are found now we want to say that rather than an okay result our result should be of type not found result so now in fact we get a failure here because what's happening is we're actually returning an okay object result and not a not found result so let's write just enough code to return a 404 here in our users controller let's get our users and then we can make this like an assertion here where if results are rather users got any only in this case do we return an okay result otherwise we return not found okay now we'll run our tests and we can see that now we have two tests failing although our new 404 test is returning green and so we need to now fix the failing test before we move forward so get on success returns a list of users here we made an assertion that given that we get a new list of users back we get an ok object result response back and that's not the case we know now that by design when we have an empty list of users we get a not found result so let's actually create a user here and we can use target type new here we don't need to specify the type since we have a new list of users say id is equal to one [Music] name is equal to jane address is equal to a new address zip code 53704 and jane here has an email janet example.com okay so let's go ahead and run our test again and now we have successful returns list of users test and now we'll check on why our last failing test is failing that's because again we are assuming that returns an async empty list of users we're getting a 200 response and we know now that this is actually a 400 response in fact we can write a test asserting that that's the case you can do that here towards the bottom as well get and we'll use the same syntax here we'll get on no users found and in fact for now what i'm going to do is just include this as part of the other test result dot and save our object result is not found result and we can say object result dot status code dot should dot b 404 okay so that'll be an assertion we make and here again we want to assume that given that we have any users returned at all and so for this i'm going to bring in this returns async block and then we'll consolidate some of this into a helper class to make things a little bit more manageable for us okay let's go ahead and run all of our tests and our tests are green okay so we've tested some basic controller behavior both sort of happy path when we have users and a path and we don't have users i can see some places here where it would be really convenient to have sort of some test fixtures for instance for generating new users and so that's why we've created this fixtures directory so let's go ahead and create a user's fixture and we're going to make this a static class i'm going to paste some code in here for this and so all we have here is a static clasp the single static method get test users and it's just an expression style method that's returning three test users for us and you can see that it's just populated with some test data here now you can get as elaborate as really your system requires over time when you have different sort of variations of collections of users or single users that you need to be able to return and there are certainly libraries that that help with that including things like faker to generate some sort of fake test data but in our case for a simple example i'm just showing how you might create a fixture to return some data that you can control and reuse relatively easily so for this we'll head back into test controllers and then rather than return sort of this inline list here we can say user's fixture we can bring this in we'll say get test users and we'll do the same for the other method where we sort of had an inline user created for tests which is here okay so that sort of cleans our code up a little bit let's go ahead and rerun our tests again and make sure that everything's working correctly okay so we're all good so let's turn now to do some test driven development on the service layer which we know is just not implemented at the moment what this should demonstrate though is that you know unit tests help us test our systems in isolation so even though our our user's controller here has a dependency on a user service we don't actually need an implementation of that user service to test the behavior of the class that depends on it we can test the behavior of our controller class completely independently basically relying on the design of user service that is the interface of the user service to basically make assertions about how the user's controller acts in turn so we're testing the implementation of user's controller here and we're only depending ever on the abstraction of any of its dependencies so now let's test the actual implementation of our user service so we'll come back into our unit test project and under systems i'm going to create a new subdirectory the same level of controllers called services and we're going to create test user service so there's our test user service class and in fact i want to rename this to users service okay we'll make this a public class and so how do we want our user service to behave well we have this method get all users and when that method is called what we really want to happen is for this service to make an http request of its own out to some separate you know third party service itself so one way we could look at that is saying get all users when called or when invoked itself invokes an http get request so as mentioned we're kind of proxying a request out from our client to some third-party dependency that can get us some data that's required for us to sort of serialize and return to our own client so as usual we have an arrange act and assert and what we'll have is some system under test which is our user service note now that we're of course testing the actual implementation of our user service within these tests and when we call system under test.getall users what we want to do is somehow verify that an http request was made all right so here's where things perhaps get a little bit more tricky a little bit more advanced from a design perspective we have to know a little bit about what's available to us in net to do this in a really sort of conventional way and so one of the things that we have access to is an http client object in.net what this allows us to do is to basically create a client server side here which itself makes an http request and we can pass that http client as a dependency to our service layer and in fact the way that we do this in.net is by using an http or an http client factory so let's go take a look at what that might look like from the perspective of our service so we have a very bare bones user service right now and really the way that this works traditionally in net is to say we have some backing field here http client okay and now we need to just sort of figure out how we want to inject this so of course for this to inject anything we need a user service constructor and what we can actually do now is actually just set this backing field to http client that gets passed to this constructor now in order to wire this up properly we need to head back into program.cs which we'll do here and basically the factory if you will can be handled through dependency injection again just in the same way that we've added a transient instance of a user service implementation when we request the user service interface we also have a method here called add http client and we can specify that our user service should get an instance of the http client this particular implementation of user service should get this http client okay so if we head back into our user service now that's actually sufficient to wire up our hdb client for use so if we head back into our test finally we now need to basically make a mock for that http client so that we can provide it to a new instance of our user service that we're trying to test and i'll note that if you were to try this for the first time without ever having created a mock http client you may run into a few issues in basically setting up everything that you need to set up to have a sufficiently mocked out http client to work with here and so to save you some time what we'll do is we'll basically create this as its own class that can be reused in many places because there are a few lines of setup to basically consider here so what we're going to do is in our helpers directory i'm going to add a new class and we're going to call this a mock http message handler this can be internal to the test project it's just for unit testing purposes to stand in place of a real http client it's going to be a static class again with a single static method which itself is a mock http message handler so this method is going to return a mock so we'll bring in mock and we're going to call this mess method setup basic get resource list and so we're going to set up our mock to return a list of resources resource in the general rest sense where we have a list of type t and our mock message handler will be generic for that type as well and i'll correct the spelling on message handler apologies for that and that exists on system.net.http and we'll call this the expected response that we want this mock to return so we're creating a method that's going to basically return to us a fully set up mock http message handler that returns some expected response for us so this is just a convenience method we'll say our mock response is a new http response method or a message rather which returns the status code okay with content as new string content of that expected response is basically serialized and then we'll set our content type headers on the response as a new media type header value of application json okay let's uh make things a little bit easier to read for us here now and now finally our handler mock the thing that we actually need to return is a new mock of http message handler and in order to mock this out we need to call handlermoc.protected dot setup and for this we need to bring in mock protected this allows us to set up a protected method on the mock and it's going to return an http response message now under the hood what's happening here is regardless of the verb we're always calling send async and we can say it expression which allows us to basically specify in this protected version of the mock that when we call send async with any http request message and the next argument is any cancellation token returns async our mock response and then finally we return that handler mock back to the caller so i wanted to provide this as sort of a shortcut for you rather than need to work through the underlying implementation details of an http client basically what's happening is we need to basically mock out this send async such that what we ultimately get in return is some controlled mock response given that any http request message is sent and cancellation token is the the next argument to send async here and so rather than have you sort of dig through the source to have to figure that out i figured we could just wrap this in a in a helper method here that you can reuse yourself note that if you expect the endpoint to return something other than a list of a particular type you would need to write in a different overload for this uh this sort of mock setup method okay so with that created let's head back to our test and now we can create our mock http handler with some expected response which let's say is our test list of test users which we already have a helper for in our users fixture now we can pass our handler mock object to our user service and as far as the user service is concerned it's basically using a using http client here and now we can create an http client and pass our http handler mock object which will control the behavior of this http client to return our expected response of our list of users and now we can basically set up our user service with this basically fully mocked http client so the http client is a real http client whose http message handler is our mock which basically controls the type of response we're getting back okay so when we call get all users now we want to verify that an http request is made so how do we verify this well we need to look at our handler mock which is the basically the underlying implementation detail inside of http client that's making that request and again that's a protected member so we need to bring in mock protected you can see that here we need to verify that send async was called exactly once this we can bring in mock of course and we want to make sure that it's a get request so we say it expression is http request message request request.method is equal to httpmethod.get and the second argument is uh just that cancellation token so we can put that in very quickly here caught exactly once the method is get and it is any cancellation token an extra friends over here okay so that was kind of a lot in one test most of it just involved setting up a usable http client mock but basically the idea is that given that we're going to get some list of users back from this http request let's say that the third party http endpoint returns us objects of users that are in the shape that our users are in if not we could create a separate user type that mapped specifically to that dependent set of resources and so we create the new http client with that mocked out handler we new up our user service using that client we call get all users on the user service we basically assert that the handler made a get request so exactly one time so let's go ahead and run our unit test and see if if it passes it should fail and we can see that we can actually visit why it failed not implement an exception methods not implemented well now is our chance to actually implement it so we'll head back into our user service and think about how to write the minimal amount of code actually get this to pass well to make a get request with our http client let's make this async and we can basically say like some response it's called a user's response is awaiting http client get async to some request url okay we don't actually have that url yet and in order to get to compile we need to return a new list of users at the very least okay this should be just enough code to get our test to pass because we're actually making an http request now in fact we do see that it passes and so now let's write a new test to actually implement more of the specified behavior so what else does our service actually need to do in order to sort of specify you know its specification we know that it is making an http request when get all users is called but we haven't made any type of assertion about you know exactly where it's going what type of data it's getting and basically fulfilling its actual implemented behavior so for one thing we could assert that it returns a list of users so let's set some things up as we did previously and here we'll assert that the result is a type list of user for this we just need to bring in fluent assertions and system collections generic okay and we'll run our unit test again okay so we didn't even have a failing unit test that time that's good but that's because we're right now returning just an empty list of users from our user service so let's get a little bit more specific here let's let's assume that when we make an http request out let's assume that when we make an http request out if we get a 404 from our endpoint we're going to in turn return an empty list of users to our client which is our controller in this case which we designed such that when we have an empty list of users we return a not found result of our own so if we come back into test user service let's go ahead and create such a method that we could say set up return 404 we'll go ahead and create this method and what we'll do is we'll set everything up as we had previously albeit without any type of expected response that's really required so let's just say we have some empty string content here and the status code is not found everything else is set up this way and when we return we're basically going to be returning a not found response so we're setting that up and instead of an object here we do still need to return our mock http message handler and in our case we're going to say the result count in this case should be zero so we're going to basically try to gracefully handle that 404 and essentially interpret that from our you know middleware if you will from our service layer as being an empty list of users that we're going to return to our controller so if we try to run our test now this should actually fail well it may pass but it's not going to sort of pass for the right reason actually that's because we're always returning an empty list of users if we basically return more than one user we should return those users back to the client but in the case of a 404 explicitly we're going to always return 0. so let's head into our test user service rather our user service sorry and we'll say if the users response.status code is not found then we'll return a new list of users otherwise we want to return basically the serialized list that we get back from that particular client and so for that what we can do is we can say that our response content is some user response dot content and all users on that response are response content dot read from json async and we'll return all users dot to list okay so for to run this test everything is still passing and that's good but you can see that well a few things here in one case we want to make the assertion that we're returning a list of the correct size so given that there is a size of you know three users on the collection coming back from that external resource we want to forward that list of the appropriate size to our client we can write a test for that we also see that right now we have this sort of fake url that we're making a request to we want to make sure that this is configured and that we're reading some value from configuration which is another thing that we can test for let's take care of the the simpler thing first here come back to our user service so we'll sort of write the other side of this which is to say that get all users when called returns list of users of expected size this one get all users we should say when hits 404 returns empty list of users and so we're going to set up our mock in fact to return our expected list of users again this we know returns three users as we've set it up and so if we run our test now we would expect this to fail result count should be three and so we can see that explicitly here in the test result expected the count to be zero but found three so really the expectation here is that it should be the size of whatever the response is so we can run our test again and we get green okay so finally let's go ahead and test that you know we're pulling this value from configuration somewhere and then it's not sort of just some static config and so the way that we might do this is we could write a test so get all users when called invokes configured external url we don't really care about you know the response explicitly coming back this test is just going to be concerned with making an http request to some configured url and so we need to provide configuration to our user service to actually make this work and to do this in.net we typically use the options pattern so as we set up a user service we can actually inject configuration into it using an interface called i options so if we want to create an options for test here you might have some config we'll say options.create so we're using using microsoft.extensions.options we'll say options.create let's imagine that we have some users api options which has some endpoint on it so we have some object that has like an endpoint on it and it's https example.com users or whatever okay so we would then pass this config to our system under test we would call get all users and then we would make some assertion that our http client was invoked with the correct endpoint so let's try and set that up if we take a look at what we're actually sort of going to be testing here coming to user service in fact i'm going to close all the tabs to the right of this one basically we're assuming that httpclient.getasync is going to hard coded value here so let's say foo dot com we need to create a mock resource handler for a particular endpoint so i'm going to create a new overload for this which has some particular endpoint configured we'll call this endpoint on the config object we'll go ahead and create this new overload and we'll go visit so this is going to be set up just like the setup basic get resource list that we have previously and so in fact i'm going to move this closer to the first overload and rather than providing it with any http request message we can go ahead and make one here there's a get method so now we're setting up the handler such that when we call send async we're basically passing in the particular endpoint that we want to make and so we can pass that all the way up as we're setting up the entire mock itself and we need to make this a mock and just like the other one here this is a mock http message handler okay so we come back down here and a few things are the case first of all we don't have a user api options object and we're certainly not passing it to our user service so this test would fail because of the compilation error the way we do this in dot net is using the options pattern with this i options interface so what we would do is we would say i options here and here we bring in microsoft extensions options and the type here let's call this api config the type here is going to be in our case this users api options that we would like to have exist so what i'm going to do is under models actually we'll create a new directory here called config and we're going to create a new class for this for this i'll use the folder view again config add a new file here called users api options dot cs and here we go very simple plain old c sharp object with a single string endpoint property so we come back and this will of course be a user's api options type bring in the namespace users api.config and the way that we create a backing field for this is to actually set the backing field as the underlying type from the generic and we call api config.value so now all we need to do is much in the same way that we've wired up service dependencies using dependency injection we can head back into program.cs and down where we configure services what i'm going to do here as well is say services.configure and we're going to say map users api options go ahead and bring in this and we'll bring in microsoft extensions dependency injection here we're going to say builder dot configuration dot get section users api options have our closing brace here just so we can see everything on the same screen that's what that should look like and in fact here i'm going to actually clean this up just a bit now what's happening here is this is telling.net to resolve it from any of the sort of configuration sources in this case we're set up with appsettings.json with the default sort of host you know built host here and so if we head into appsettings.json we can actually just add a section here which includes that configuration to wire up and so all that that's required is this is just json so we can set users api options this is going to map by name to that type that we just created so we'll have an endpoint and then here let's use a json placeholder endpoint typical.com users if you're to visit this in a browser you'll see that this returns a json list of users so here we have a json list of users and this is sort of just a a test endpoint and this is going to serve as the endpoint for our little application here but you'll note that this url is itself configured rather than hard-coded within c-sharp so in the future if we needed to sort of have a secret endpoint or something a little bit more secure then we could certainly choose not to version control this configuration as part of the project or not ship it with part of the project but manage it in a standard sort of secret storage or other parameter store in our case as a test endpoint here it's fine in our app settings.json file for now okay finally we come back to our test user service users api options we can now bring in the using statement for that when we call get all users we should be able to make an assertion that our handler mock was called with the external url so as we've done previously when we're making assertions about the handler mock we can say it's the request message with get and the request request uri is equal to whatever we've actually configured here which in our case in the case of the sort of test configuration is just this endpoint example.com users next we need to make sure that we actually update all the places in our tests where we are creating our service without uh the new dependency we need to make sure that we actually use those dependencies just another reason why i mentioned that it's often useful to have sort of factories for for creating these things which would prevent us from necessarily needing to update them in so many places but that might be something to think about if this application grows over time so what we'll do is we will set up our config we'll start at the top here and we'll do the same for the others okay let's go ahead and run our tests and well we can see that one of the tests wasn't run here let's see which one that is that's this one let's go ahead and rebuild and then we'll run all of our tests okay we do have a failing test let's see if we can sort of figure out what happened here handler did not return a response message okay let's see if we can figure this one out okay so i see what the problem here is i've modified the handler mock in our method get all users when called invokes configured external url to assert that we're hitting our new uri endpoint here so the request uri should be equal to uri which we're sort of prep priming here as example.com users which is set on our users option object and if we just quickly revisit what's happening when we set up our handler mock we can take a look at that here and so what's happening is we are setting things up so that the response message is okay that our handler mock is a new instance of a mock http message handler we're setting up this protected sent async here and it's okay just to set this as any http request message we can still make an assertion about how sent async is used in our test but if we take a look back at the user service itself we can see we're still always getting async requests to foo.com so in fact if we just read what the exception is here in the failed test we expected it to be invoked one time with a get request and a request uri of example.com users but in fact with the test saw was exactly what we have coded which is that it's trying to request foo.com okay and we also have some headers and an http version 1.1 with null content but in any case the important thing is here that we are always actually making requests tofu.com so the whole reason we set up api config was so that we can actually get the end point from configuration so if we run the test now we can see that everything passes okay so what we've done is we have basically built a simple http related service that's proxying a request out to some configured url and returning that result to our own controller to pass that data back to our client as serialized json data so we have tests that should cover basically all the business logic there thanks to the tdd approach and in fact we can actually run this application and see it in action so what i'll do is i'll head back to the solution explorer and we can start the project so we'll go ahead and just click run from within visual studio configure to use ssl to avoid you can choose to trust the self-signed certificate let's see what happens if we trust it if not we can turn off http redirection we'll install the devcert and we'll ignore the warning you can see that swagger is generated for us with the user's endpoint let's go ahead and try it out and you can see in fact we get users back in the response body we could even run curl directly from our terminal and we should see that response come back in standard out here as well in fact we do so it's a little harder to read here in the terminal but but it's looking good and in fact if we were to do just the same as well of course in the browser we could get the response back in the browser as well and so there we go so this is real data that's being proxied from that test endpoint and we also have unit tests that test all of our logic in isolation from the things that it depends on so i hope you enjoyed this series if you enjoyed it i hope that you subscribe to the channel for more videos doing deep dives into net web development we're going to be using this project throughout some several subsequent projects where we containerize this and we basically deploy it to aws so stay tuned for that really appreciate if you liked and supported the channel on patreon for source code all right it's great seeing you again and i'll see you again soon
Info
Channel: Wes Doyle
Views: 110,770
Rating: undefined out of 5
Keywords: dotnet, .net 6, tdd, test driven development, unit testing, .net 6 api, web api, rest api, c# tutorial, .net 6 tutorial
Id: ULJ3UEezisw
Channel Id: undefined
Length: 98min 59sec (5939 seconds)
Published: Tue Feb 01 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.