A brief look into Test-driven Development with an example in C#

Gnnckr
DFDS Development Center Istanbul
24 min readDec 12, 2022

Looking for the answers of what, why, how and some hints

Red -> Green -> Better

Setting up the scene

Being a software developer is awesome!

It comes with the full package of many responsibilities but when you get a grasp on its different aspects, it becomes extremely fun and satisfying.

It involves encountering a problem, conceiving and designing a solution for it, implementing the code and creating the actual solution. Then of course you have to do (and continue doing) some tests and deal with the inevitable bugs that come to life as a natural part of software development. And it wouldn’t hurt to create some documentation too unless you have a photographic memory for your solo projects or you are working with other people.

Finally you may need to update your code as the requirements from your solution evolves. While you are making changes to your code, you may want to organize it differently, write some parts better, use different libraries or create your own ones to use elsewhere. This is called refactoring and it’s one of the key parts of the software development process. We’re going to visit this point again later.

There are many different “styles” you can use in this process, such as the traditional Waterfall model that has its own legacy which involves the following of different phases of the development one after the other. While it tries to deliver the contract that was set up at the beginning, it usually falls short on the ever-changing requirements of the fast-paced pandemonium, that is today.

To keep up with these requirements, short bursts of development yielding small, deliverable and working software products are widely welcomed in the “modern world” of highly changeable business structure. This kind of iterative and incremental development methodologies are the Agile Frameworks which consist of many different flavors on their inner workings while aiming something similar: bite-sized chunks of software to be released in quick succession.

Aside from the popular agile frameworks like Scrum and Kanban, there’s also Extreme Programming (XP), which was developed by Kent Beck in 1999 that intends to release higher quality software products by quickly responding to the customer requirements that are always prone to change. And of course, it elevates some of the elements of software development from one step ahead like continuous code reviews (which means always-on pair-programming) and writing automated tests for even smaller software sections, to extreme levels like receiving new requirements in the forms of automated tests only rather than documentation of specs, etc., hence the name. Creating a bit of a controversy on aspects like these, it’s not as popular or widely used as the said other frameworks, but it paved the path to something that could actually be used and bring value while not taking the things to the bleeding edge and disrupting processes discouragingly. Disrupting processes in a good way however, is why Test Driven Development is all there for.

What is Test-driven Development?

Test-driven development (TDD) involves software development in a way which seems like a counter-intuitive manner at the first glance. What this means is that, when you want to create a program for some specific need, you’d think that you need to start development by coding the actual software requirements like creating data models, the business logic, etc. You don’t do that in TDD. Instead, it goes like this:

  1. You start by adding a test. It should be verifying that a particular feature is working as intended, such as creating a customer information object when appropriate data is provided. These features can be extracted from the user stories or use-case scenarios defined in the software requirements.
  2. You run the test. Since you haven’t written any code that could actually do what the test is looking for, expect that it will fail. If it succeeds, it might (actually should) mean that there’s something wrong with your test setup.
  3. Then you write the minimum amount of code that is required to make the test pass, knowing that the code will be improved in the next steps. Now this one is phrased like this by definition but it could be improved when you are actually doing TDD. Because while this step allows and encourages a dumbed-down approach like hard-coding parameters and writing functions without thinking about tomorrow, it might save some time by skipping some steps to create a bit more depth in the code and organize it moderately in the long run. We’ll visit this one later.
  4. Run all the tests again (at the beginning, we’ve got only one but you get the idea). If they fail, the code needs to be updated to address the issues. Stay on this step until all the tests pass.
  5. Refactor the code as much as required without overdoing. Tests should be run after each change to establish that the code is still working. While doing refactoring, consider removing hard-coded bits if there are some, re-organize (or introduce) the hierarchies of classes and interfaces, remove duplicated code with local functions and perform some other refactoring goodies as needed.

And the cycle continues like this with the first step for the next feature.

Why might this be a better way than regular software development with writing unit tests?

Simple: you don’t write code any more than you need!

Code is fragile. Tiny mistakes yield big impacts on the cost of maintenance, time to spend on debugging sessions to hunt for mysterious bugs and of course, the actual health of the application that could render it useless in a heartbeat. To reduce the number of (inevitable) bugs, we are gifted with the ability to write some unit tests, which is proven to be effective. TDD also utilizes these very same development and testing platforms to perform its magic, so you might say what makes the difference then? The answer to this lies on the approach. Be prepared, light bulb moment is approaching.

When you write unit tests, you only test what you’ve coded. When you write tests at the beginning, you code what you want to test.

In other words, the actual requirements of your software guides what features your application must have. Therefore you don’t need to write any more than you need (in terms of the rest of the solution other than the tests), which would reduce the potential code digress from what the actual requirements are while “you’re in the zone”. For all the other possible improvements to the process, let’s talk a bit about the potential benefits of TDD.

Test-driven Development Benefits

First and foremost, there is the design element of course. When you create your tests at first, it shapes how your program should actually behave. It makes you think about the way the users will consume it and the actual experience. So you think what to code at first, rather than how you code it. This makes the final product more aligned with the requirements and also keeps you away from falling down in many levels of deep abstractions. You’ll find the chance to create those abstractions and organize the code structure in the oncoming refactoring steps as they become necessary, but not at the beginning when you don’t need to design a complex structure without creating anything that the user cannot touch and see for a long time.

Another positivity that TDD brings is that your test coverage will be immense at the end of your development. Because you write all the functionality via your tests before the implementation, you cover almost all code paths even to the level of comparisons. So when an update comes along way down the road which would alter the way, say the implementation inside of some control statement, your automated tests will automatically fail if they wouldn’t like the new behavior. Then you’ll have the upper hand to react to the change and decide if there’s a better way to do it.

Yes, TDD creates a bigger code base than a solution without any (or some occasional) tests but it might decrease the overall development time by reducing (almost eliminating) the need to long debugging sessions. When you cover almost all your code paths, you greatly reduce the unwanted behaviors in your program before they even arise. You shouldn’t be concerned about the number of tests that are piling up. That’s a good sign as long as they stem from the actual requirements of the application.

Last but not least, TDD encourages developing more loosely-coupled, cleaner solutions because of its incremental and bite-sized nature. Focusing on smaller portions of the requirements rather than a more complex picture makes the developers create more independent and smaller units of logic which would then be brought together. This also yields better interfaces and helps the development for tests and mock classes. Before you know it, your code becomes more modular than you’d initially think and it’s always favored!

Besides these great features of TDD, it wouldn’t suit every occasion and you might be better off not using it in certain situations or project types. Let’s see what those might be.

When you don’t need (or straight up wouldn’t want) to use TDD?

Bear in mind that a good way to use TDD is to test for how a program behaves, rather than how its logic is actually implemented. This is not easy when you want to use TDD for user interfaces where testing such structures requires extensive unit tests to be written and a more thorough dive into the implementation details, which is against the vision of TDD.

Also network and database dependencies of a program may alter, slow down or even halt the execution together with the tests and it’s not a good idea to use TDD on these portions of the programs either. Rather you should use TDD for the core library classes and perform most (if not all) of the business-critical logic here.

If this doesn’t sound like a good idea for your particular case, using TDD on this kind of a project might be unnecessary.

Continuously changing requirements is another cause for concern on deciding whether using TDD is actually beneficial. If your particular situation is subject to rapidly changing requirements, you might need to continuously alter the tests along with the feature development. If this is not easy or viable to keep up, maybe dropping TDD is a better idea.

In addition to these, you would of course need the support of the managers. Since you don’t start with creating the actual requirements (at least it doesn’t seem that way) at the beginning, they might perceive the time spent on writing tests as waste. Good communication is key for them to understand here and the only way to make them believe that this approach will produce better results in the long run.

If none of these is an issue and you decide to use TDD, there are also some pitfalls on the way which you should avoid. Let’s have a look at some.

Pitfalls to avoid when using TDD

Since writing the tests and developing a particular requirement is usually done by the same person, it might be easy to miss some steps in what is needed and you can easily find yourself in a situation where you don’t have any tests that would actually check for it. The feature would even be straight up wrong if the developer has a different understanding than what is actually required. Gaps like these must be looked for and addressed by occasional cross-checks between the required parties.

You should avoid writing tests that could be easily broken, rendered useless and ignored easily. Like we’ve mentioned earlier, the tests should be testing for behaviors rather than implementation details. They must observe what’s happening rather than creating more dependencies to some unnecessary parts of the system or even their own implementation details. So avoid hard-coding in the tests and use mocks, utilize interfaces and stay loose.

In relation to the above concern, we can add that the maintenance of these bigger (and getting even bigger!) number of tests will become the burden of the project and if they start to fail in increasing numbers beyond a repairable state, they will be ignored and might even be deleted. This would create unaccountable portions of the code and would be very hard to dig through to resurface in a development environment where you have the confidence that your tests are making the hard work. To avoid this point, you might consider creating less-detailed or higher level tests which would depend less on other structures.

Passing tests could falsely indicate that everything is in order. That might not be the story outside the scope of the current project and you might tend to overlook an integration misbehavior with another system for example. You should consider taking appropriate steps to secure the execution of your solution as a whole. Using TDD should not make you leave other good practices of software development.

You should already be asking if there aren’t any best practices along with these for some time. Of course there are, here you go.

Best practices when using TDD

We’ve mentioned a lot of them in the discussion above as what to do instead in some situations but to sum them up, let’s make them clearer.

You should always write your tests against an interface, not to an implementation. Remember, you’re testing the behavior and not the way how it works. Implementation is almost always subject to change. Interfaces tend to have that less or ideally none. You don’t need to create a test every time you add a new class. Create one to implement a requirement!

You should use mocks rather than test doubles. Again, this (in a way) supports the one above and extends it. Sure, it’s rather easy to use test doubles or stubs but they don’t reflect the actual code that’s being tested. Mocks on the other hand are there for you to be able to test the behavior. Use them.

If a test is hard to write, usually the underlying design is bad. If the mocks are too tightly-coupled in the test, usually it means that the design is too tightly-coupled to the thing that is being mocked. If that’s the case, you should consider changing the design.

Write simple tests for simple code. So write testable code from the start. You should always re-organize and add more complexity later when required, not before.

Tests should be covering the user requirements or use cases. Given-when-then model is ideal to have it in mind. There is also an Acceptance Test-driven Development (ATDD) methodology in where basically the requirements arrive in the form of some tests in the given-when-then format above. You can utilize it to have a simpler model of understanding requirements.

You should generally avoid features that require network and database connections or file system interactions as they would generally slow down the tests and actually prevent you from running your tests in isolation from other tests, as we’ve mentioned before.

Enough of non-practical information? Let’s do TDD together!

A practical approach

Software development usually starts with some requirements at hand. Like we’ve mentioned before, TDD it’s a bit of a design-first thinking rather than going deep into the development of the features first. So we’ll look at the first requirement, write our first test, make sure it doesn’t pass, write the minimum required code to make the test pass, refactor and organize the code we’ve just written and continue with the next requirement.

For demonstration purposes, we’ll take baby steps and create a very simple class library in C# that’ll help us understand what actually happens in TDD.

Let’s say we’re asked to build a simple system where users can create a specific purchase order and it’ll then be stored in a database. For this system, say that the first two of our requirements are like the following:

  • Users should be able to create a purchase order and get a status message of the request once it has been processed.
  • The purchase order should be stored in a database and in addition to the status message, information about how many of that particular product is ordered and the product identification should be returned.

There will be lots of other requirements for a real world scenario of course, but these two will do for now.

Here the famous Red-Green-Refactor cycle comes in. For those who don’t know this one, it basically means that you write a test for a new functionality but the test will fail because there is no actual code implemented yet, so the test becomes Red. Then you implement what is required and re-run the test. Once the test is passed, it becomes green. Then you need to perform some refactoring and the cycle continues for the other features.

To begin our demonstration, we need to create a new unit test project.

You can use NUnit, xUnit.Net or MSTest. If you’re not seeing them in the list, try changing the filters above.

Now you basically have three options: NUnit, xUnit.NET and MSTest. These are all testing frameworks aiming the same thing while having some differences on their own.

So NUnit is based on another framework (JUnit, and they both have roots on xUnit which was derived from SUnit) written for Java and has been in service for a long time.

xUnit.Net is an improved version of NUnit and a well-adjusted framework for TDD. It reduces code duplication by eliminating the SetUp and TearDown attributes of other xUnit frameworks and because it works by utilizing the constructors of the test classes and the IDisposable interface for releasing resources, it helps to create much cleaner tests.

MSTest is the default testing platform for Visual Studio and is shipped with it. It eliminates the need for a third-party testing tool and has a similar syntax with the xUnit.Net.

We’re going to use xUnit.Net for our demo. So select the xUnit Test Project under the .Net Core section. If you don’t see it, you can select a Class Library as the project type. Or you might choose to try the NUnit framework side by side. We can always add the xUnit.Net package to our project later. I’ll also show it in a moment but let’s give the test project a proper name.

Whether your new project is any kind of a test project or a class library, give it a meaningful name.

Ending with “.Tests” suffix to the name of your project could work great. Since we’re going to be dealing with a system that will process hypothetical orders, let’s call it an OrderSystem. So our test project should be named accordingly. Make sure the solution name doesn’t include the suffix.

After you’ve set up the naming, click the Create button.

Clicking the Create button after the name sets our project with a sample NUnit class if we’ve selected NUnit.

To add the xUnit.Net framework to our project, open the NuGet package manager and search for it. You should be installing the one written by James Newkirk and Brad Wilson (the top one in below screenshot, it’ll also install some dependencies. Also we’ll need one more package later).

Search for xunit in the NuGet package manager.

It’ll install the following dependencies.

The dependencies for xUnit.Net 2.4.1.

Clicking Ok will add the framework to your project. This way you wouldn’t have to worry if you’ve selected a type of project other than xUnit Test project.

xUnit.Net is shown installed with the green down arrow under its logo.

Now that we’ve set up mostly everything we need, we can start writing our first test for our first requirement.

So to recap, our first requirement was the following:

  • Users should be able to create a purchase order and get a status message of the request once it has been processed.

So we can say that we’ll need a processor to handle the purchase order for a start. Let’s add a new test class for our tests while also deleting the sample UnitTest1 class and give it the name PurchaseOrderProcessorTests.

Add a new class with the appropriate name.

Let’s now start our test by creating an instance of our processor that’s currently not implemented.

var processor = new OrderProcessor();
Everything starts with our processor instance, which is not there yet!

Visual Studio will surely complain about we’re not having the OrderProcessor defined elsewhere. Select the Generate class in the new file option from the error explanation. Don’t worry about the directory structure for now. Give it the name OrderProcessor.cs.

With our processor, we’ll be processing a purchase order request. Say that our first requirement indicates that the necessary data model to be sent to the API in front of our class library consists of a product id, quantity, date of purchase, user id and a notes field.

So let’s continue writing our test with the following.

var request = new PurchaseOrderRequest
{
ProductId = 12,
Quantity = 5,
DateOfPurchase = DateTime.Now,
UserId = 1101,
Notes = "Sample Notes"
};
Create the data model of our request as directed by our test.

After adding the new file PurchaseOrderRequest.cs and the properties as depicted in our test, now we need a method that will actually do the processing and return the result of its operation. So add the following code next.

PurchaseOrderResponse response = processor.CreatePurchaseOrder(request);

Also add the following property to the PurchaseOrderResponse.cs after creating the file.

public string Message { get; set; }
Make sure you create the method definition in the OrderProcessor.cs as suggested. Make sure not to actually implement it yet, leave it as throwing an exception.

For our first assessment, we might check if we have an actual response. To do this, add the following code.

Assert.NotNull(response);

Since we’ve not actually introduced any of the testing frameworks to our class, select “using Xunit;” in the error pop-up to give the Assert statement a meaning.

Select using Xunit to add it to the class and make sense of Assert.

We also need to add a [Fact] attribute to our test method to indicate that it actually is a test method.

Add [Fact] attribute to the test method.

Let’s add another Assert to check if the response has a message. We can go with the happy path for now and refactor the actual implementation and the tests where necessary in the future. Add the following code.

Assert.Equal("Purchase order stored successfully", response.Message);
Basically we’re stating our expectation in the first parameter and the actual live data in the second parameter.

Now it’s time to run our first test! With great excitement, we open the Test Explorer and click the green arrow thingy but nothing happens.

Test doesn’t run despite smashing all kinds of different run test(s) buttons.

Because we need a final package to add to our solution. Open up NuGet and install the xunit.runner.visualstudio package which will run our tests.

Add the final piece of the setup.

Now, finally we’re ready to run our test. Running it obviously and immediately fails because we’ve not done any development to make it pass. This marks the Red state in the famous Red-Green-Refactor cycle and makes way to the green state, where we actually implement the functionality by writing the minimum amount of code that’s required to make the test pass. No concerns on code quality and no high level thinking is required and encouraged here. Just the bare minimum is required.

Our first test has failed! What a surprise!

To make the test pass, we only need to return a non-null object and with the message that we’re expecting. Yes, this is still far from an actual development but we’ll get there. Just add the following code to the CreatePurchaseOrder method while removing the throw statement.

PurchaseOrderResponse response = new PurchaseOrderResponse
{
Message = "Purchase order stored successfully"
};
return response;
After adding this minimal code, we are ready to run our test again.

Running the test now will give us the green light!

Our first test is passing with tears in our eyes!

This concludes the Green part of the Red-Green-Refactor cycle. Now as we’re in the Refactor state and we need to make some changes to the project structure, we could remove hard-coded places if we have and generally organize it better to have an easier foundation for future builds.

In order to define our logic more properly, it’ll surely be a good idea to have a separate class library where we can expand our code. So add one to the solution and give it a meaningful name. OrderSystem would work for our purposes.

Add a new Class Library project to our solution.

It automatically comes with a sample class (Class1.cs). Remove this one, move all the classes except the test one to here from the test project and fix namespaces by removing “.Tests” suffixes.

Carry the classes to the new project and fix namespaces.

Set all access modifiers to the public to be able to be accessed from the test project.

Leaving in internal or private states would prevent the test project accessing the required resources.

Hard-coding stuff is not really a good practice, so we can add a static class for these kinds of constants and use them where needed. This way we will have greater control on the resources and less maintenance overhead for the upcoming updates throughout the life of the application. So let’s add a new class to hold our constants.

Add a new class with the name Definitions.cs to the class library.

Make this new class static and copy the message that we’re returning as a static string.

We have a constant that we can use anywhere and update from a single point. Try not to make a typo like me!

We should also not forget to use this new constant wherever we’re using the hard-coded message.

Change the hard-coded message line in the OrderProcessor to use the constant.

We should not forget to add this new class library to our test project as a reference to be able to access its resources.

Right click the Tests project and add the class library as a reference.

As we now have access to the resources of our class library from the test project, let’s use the new constant in the tests too. Change the hard-coded message bit to use the constant.

Use the constant in the test.

We’ve made a bunch of updates for refactoring, let’s see if our test still passes. Head to the Test Explorer and run the test again. If everything went well, it should pass.

It does!

Now that we’ve completed what is needed for our first requirement, we can move on to the next one.

To recap, our second requirement was the following:

  • The purchase order should be stored in a database and in addition to the status message, information about how many of that particular product is ordered and the product identification should be returned.

Things are a bit more complex here than before because we need to create a database action in our code. For this one we need an interface that we can use with both performing the real database actions and also with the fake ones that we’ll use from our tests. Decoupling is almost always a good idea for separation of concerns and this is a pretty example to apply the idea.

So let’s start again by adding another test which greatly resembles the first one. After we set up some items, we’ll revisit this test. Add the new test like the following:

[Fact]
public void ShouldStoreInTheDatabase()
{
var request = new PurchaseOrderRequest
{
ProductId = 12,
Quantity = 5,
DateOfPurchase = DateTime.Now,
UserId = 1101,
Notes = "Sample Notes"
};
PurchaseOrderResponse response = _processor_CreatePurchaseOrder(request);}

Now we need to add an interface with the following name:

IPurchaseOrderRepositoryInterface
Right click to the class library and add an Interface with the name above.

This interface should only have one simple method declaration that takes a PurchaseOrderRequest to try to Create a record. We’ll also add a return value for the result of the operation as of type string. Copy and paste the following code into the interface.

string Create(PurchaseOrderRequest request);

Now we also need a mocking library to perform a simulation in our code where we don’t want to use external systems. A mocking library could be helpful for us if we need a certain behavior set up in any way that we want from any place in our system. This could be an external API call, a database operation, a particular business logic calculation, etc. We’ll use a mocking library to perform a simulated successful database creation action for our purchase order request without the actual need of a real database implementation.

Moq is one of the useful packages that we can use here. So let’s add it from the NuGet package manager.

Right click on the Tests to get to NuGet and add the Moq package.

Coming back to our test, we should write the code to inject this new interface. While we’re doing that, as our new test will also be using the OrderProcessor, let’s move its declaration to the constructor and use it rather than defining a separate one for each of our tests. To be able to do that, we should add two private fields to our test class:

private readonly Mock<IPurchaseOrderRepository> _purchaseOrderRepositoryMock;private readonly OrderProcessor _processor;

And add the following to the constructor of our test:

_purchaseOrderRepositoryMock = new Mock<IPurchaseOrderRepository>();
_processor = new OrderProcessor(_purchaseOrderRepositoryMock.Object);

Now we are able to make the Create method behave however we like. We’ll be expecting it to run its own code to relay the database operation to whatever database system the actual implementation of it is (or will be) programmed to perform and return the result of the operation in the end.

Let’s start adding it bit by bit to our test. Add the following code to the start of our new test:

PurchaseOrderRequest processedPurchaseOrderRequest = null;_purchaseOrdereRepositoryMock.Setup(x => x.Create(It.IsAny<PurchaseOrderRequest>()))
.Callback<PurchaseOrderRequest>((mockedRequest) =>
{
processedPurchaseOrderRequest = mockedRequest;
});

In addition to the return message, we also need the product id and the quantity from the request as per the requirements. It’s also a good idea to check if the request is not null. Let’s add these controls slowly to our test.

_purchaseOrderRepositoryMock.Verify(x => x.Create(It.IsAny<PurchaseOrderRequest>()), Times.Once);
Assert.NotNull(processedPurchaseOrderRequest);
Assert.Equal(request.ProductId, processedPurchaseOrderRequest.ProductId);
Assert.Equal(request.Quantity, processedPurchaseOrderRequest.Quantity);

Checking for these four items makes it sure that the request is not null, the create method is hit exactly one time and the product id and quantity information from the received request is actually the ones we’ve sent in the first place and doesn’t get changed in their way. It seems trivial to check for this kind of change in such a simple code but consider a scenario where you’ll be adding some anti-corruption layers and health checks, etc. which might have the potential to alter the request in some unknown way for now, at this point in the lifetime of the product. It’s better to be safe than sorry.

We didn’t add a check for the return message for now because there would be a better way to implement it than its current form, so we’ll be dealing with it shortly. Now, we are ready to run our second test while expecting it to fail.

Open the Test Explorer and run all the tests.

As expected, our new test failed since our processor lacks the latest changes required.

Now we need to inject our interface to the OrderProcessor to use it. So head to the OrderProcessor class and add a private field with the following code:

private readonly IPurchaseOrderRepository _purchaseOrderRepository;

And replace the constructor of the class with the following:

public OrderProcessor(IPurchaseOrderRepository purchaseOrderRepository)
{
_purchaseOrderRepository = purchaseOrderRepository;
}

We should also need to call the Create method of the repository in our CreatePurchaseOrder method and while we’re here, we should enhance the return message with the id of the product and the quantity information besides the return message. So change the inside of the method into the following:

_purchaseOrderRepository.Create(request);PurchaseOrderResponse response = new PurchaseOrderResponse
{
Message = Definitions.SUCCESSFULL_PURCHASE_ORDER_MESSAGE,
ProductId = request.ProductId,
Quantity = request.Quantity
};
return response;

Don’t forget to add the new fields of ProductId and Quantity to the PurchaseOrderResponse class as of type int.

Now if we try running our tests again, we should see them all passing.

Yay! It passes.

We’ve arrived at the refactor stage for our second test. One thing that you might be wondering for some time could be that the return message is always returned as our constant value of success regardless of the database operation and this hardly makes any sense.

The expected implementation for this could be populating the Message field of the response with the result of the Create method. That way our repository will return what actually happened while creating our purchase order. To do that, move the Create call to where you’ve put the constant as shown in the following.

Move the Create method to populate the Message field with the actual result.

Now to add a control for this, head back to our second test and program the mock to actually return a message. To do that, add the following right before the Callback function declaration.

.Returns(Definitions.SUCCESSFULL_PURCHASE_ORDER_MESSAGE)

Finally, check that message if it actually returns one by adding the following as a final control.

Assert.Equal(Definitions.SUCCESSFULL_PURCHASE_ORDER_MESSAGE, response.Message); 

You should also change the control for other fields of the request by changing the other two Assert statements with the following:

Assert.Equal(processedPurchaseOrderRequest.ProductId, response.ProductId);
Assert.Equal(processedPurchaseOrderRequest.Quantity, response.Quantity);

If everything went well, you’ll see the new test is passing when you run them all.

The new test is passing while the first one is failing with our latest changes!

Oh no! Our first test is failing now. It does have an actually very simple explanation though.

Since we changed how the OrderProcessor works by removing the constant and adding the result of the repository action Create, our first test now lacks the configuration of this Create function to make it return a value to the message field.

Our last Assert waits for a message that’s never going to arrive.

What do we need to configure? Yes you are right, we need the mock for this one too. So add the following to the start of the first test.

_purchaseOrdereRepositoryMock.Setup(x => x.Create(It.IsAny<PurchaseOrderRequest>()))
.Returns(Definitions.SUCCESSFULL_PURCHASE_ORDER_MESSAGE)

Now we’re all set, let’s fire up all the tests for a final time!

All of our tests pass, all of our requirements fulfilled, our job for now is done!

Seeing all our tests pass concludes our exercise. We’ve actually completed the requirements and we have our test structure which covers the expected behaviors of all of the requirements at the same time. Job’s done!

It must be tough to follow everything through these code pieces and low-res screenshots and my “successfull̶”! abstraction of a simple concept in a rather busy narrative. But this was only to provide some insights for you so that you can find your own way and practice a little bit easier.

TDD could be really beneficial if it suits your project’s particular needs. If it doesn’t, you don’t have to use it at all as long as you write your tests and keep your code coverage to a certain level.

Hope you’ve found something valuable for you in this article if you did make it up to here.

--

--