📜 ⬆️ ⬇️

TDD for beginners. Answers to popular questions

Source project written using TDD. Visual Studio 2008 / C #
The xUnit library is used for writing tests, and Moq is used to create mock objects.




At the next interview, asking about TDD, I came to the conclusion that even the basic ideas of developing through tests are not understood by most developers. I believe that ignorance of this topic is a big omission for any programmer.
')
I get a lot of questions about TDD. Of these questions, I chose key questions and wrote answers to them. The questions themselves can be found in the text, they are in italics.

As a starting point, we will solve a business problem:

A task consists of several subtasks: 1) write a console application that sends reports. 2) Every second report generated should also be sent to auditors. 3) If no reports have been generated, then we send a message to the management that there are no reports. 4) After sending all the reports, you need to display in the console the number of sent.

Question: How to start writing code with TDD?

We start with the fact that we will solve one small problem. We will describe the business requirement in code using a test.


So, I created a new project of the console application (the final code can be downloaded from SVN ). Now there is not a single line of code in it. We need to figure out how my application will work. Attention, the beginning! Create a test that describes the 4th requirement:

public class ReporterTests
{
[Fact]
public void ReturnNumberOfSentReports()
{
var reporter = new Reporter();

var reportCount = reporter.SendReports();

Assert.Equal(2, reportCount);
}
}


* This source code was highlighted with Source Code Highlighter .


The Assert class checks whether the number of sent reports is equal to 2. The test is started by the console utility xUnit , or by some plug-in to Visual Studio.

We have just designed our application API. We will use the Reporter object with the SendReports function. The SendReports function returns the number of reports sent, this is shown by a test using the Assert.Equal statement. If the variable reportCount is not equal to 2, then the test will not pass.

At the first stage of the design is over, go to the coding. Let's write a minimum of code to make this test work.

Question: First we need to write a lot of tests, and then fix them one by one?
No, we go consistently. We will not disperse their efforts. At each time only one idle test. We will write the code that this test will fix and we can write the next test.


In our project at the moment there is only one class ReporterTests . It's time to create a tested Reporter class. Add a Reporter object to the project and create an empty SendReports function for it . In order for the test to pass, the SendReports function must return the number 2. It is not yet clear how to set the initial conditions in the Reporter object so that the SendReports function returns the number 2.

We return to the design. I think that I will have a separate class for creating reports, and a class for sending reports. The Reporter object itself will control the logic of the interaction of these classes. Let's call the first object IReportBuilder , and the second one - IReportSender . Designed, it's time to write the code:

[Fact]
public void ReturnNumberOfSentReports()
{
IReportBuilder reportBuilder;
IReportSender reportSender;

var reporter = new Reporter(reportBuilder, reportSender);

var reportCount = reporter.SendReports();

Assert.Equal(2, reportCount);
}


* This source code was highlighted with Source Code Highlighter .


Question: Are there any rules for naming test methods?
Yes there is. It is desirable that the name of the test method indicates that it is testing the test and what result we expect. In this case, the name tells us: "The number of reports sent is returned."


How the classes that implement these interfaces will work now does not matter. The main thing is that we can form IReportBuilder all reports and send them using IReportSender .

Question: why it is worth using the IReportBuilder and IReportSender interfaces instead of creating specific classes?
You can implement the object for creating reports and the object for sending reports in different ways. It is now more convenient to hide the future implementations of these classes behind the interfaces .

Question: How to set the behavior of objects with which our test class interacts?
Instead of real objects with which our tested class interacts, it is most convenient to use stubs or mock objects. In the current application, we will create mock objects using the Moq library.


[Fact]
public void ReturnNumberOfSentReports()
{
var reportBuilder = new Mock<IReportBuilder>();
var reportSender = new Mock<IReportSender>();

// IReportBuilder
// : " CreateReports List<Report> 2 "
reportBuilder.Setup(m => m.CreateRegularReports())
.Returns( new List <Report> { new Report(), new Report()});

var reporter = new Reporter(reportBuilder.Object, reportSender.Object);

var reportCount = reporter.SendReports();

Assert.Equal(2, reportCount);
}


* This source code was highlighted with Source Code Highlighter .


We run the test - it fails, because we did not implement the SendReports function. We program the simplest possible implementation:

public class Reporter
{
private readonly IReportBuilder reportBuilder;
private readonly IReportSender reportSender;

public Reporter(IReportBuilder reportBuilder, IReportSender reportSender)
{
this .reportBuilder = reportBuilder;
this .reportSender = reportSender;
}

public int SendReports()
{
return reportBuilder.CreateRegularReports().Count;
}
}


* This source code was highlighted with Source Code Highlighter .


We start the test and it passes. We have implemented the 4th requirement. At the same time recorded it in the form of a test. Thus, we compile the documentation of our system. As practice has shown - this documentation is the most current at any time and never becomes outdated. Go ahead.

Question: Is there a standard pattern for writing a test?
Yes. It is called Arrange-Act-Assert (AAA). Those. The test consists of three parts. Arrange (Set) - we make setting the input data for the test. Act (Act) - perform the action, the results of which are tested. Assert (Check) - check the execution results. I will sign the corresponding steps in the next test.


Now we will deal with the first requirement - sending reports. The test will check that all generated reports are sent:

[Fact]
public void SendAllReports()
{
// arrange
var reportBuilder = new Mock<IReportBuilder>();
var reportSender = new Mock<IReportSender>();

reportBuilder.Setup(m => m.CreateRegularReports())
.Returns( new List <Report> { new Report(), new Report()});

var reporter = new Reporter(reportBuilder.Object, reportSender.Object);

// act
reporter.SendReports();

// assert
reportSender.Verify(m => m.Send(It.IsAny<Report>()), Times.Exactly(2));
}


* This source code was highlighted with Source Code Highlighter .


Question: Do I need to write tests for all objects of the application in one test class?
Very undesirable. In this case, the test class will grow to enormous size. It is best to create a separate test file for each class under test.


We run the test, it does not pass, because we did not implement the sending of reports in the SendReports function. On this, as usual, we complete the design and proceed to coding:

public int SendReports()
{
IList<Report> reports = reportBuilder.CreateRegularReports();

foreach (Report report in reports)
{
reportSender.Send(report);
}

return reports.Count;
}


* This source code was highlighted with Source Code Highlighter .


We run tests - both pass. We have implemented another business requirement. In addition, by running both tests, we made sure that we did not break the functionality that we did 5 minutes ago .

Question: How often should I run all the tests?
The more the better. Any change in the code may unexpectedly affect you in other parts of the system. Especially if this code was not written by you. Ideally, all tests should be run automatically by the integration system (Continuous Integration) during each build of the project.

Question: How to test private methods?
If you have read this far, you already understand that once the tests are written first and then the code, then all the code inside the class will be tested by default.


It's time to think about how to implement the third requirement. Where do we start? Will we draw UML diagrams or just meditate sitting in a chair? Let's start with the test! Let's write the 3rd business requirement in the code:

[Fact]
public void SendSpecialReportToAdministratorIfNoReportsCreated()
{
var reportBuilder = new Mock<IReportBuilder>();
var reportSender = new Mock<IReportSender>();

reportBuilder.Setup(m => m.CreateRegularReports()).Returns( new List <Report>());
reportBuilder.Setup(m => m.CreateSpecialReport()).Returns( new SpecialReport());

var reporter = new Reporter(reportBuilder.Object, reportSender.Object);

reporter.SendReports();

reportSender.Verify(m => m.Send(It.IsAny<Report>()), Times.Never());
reportSender.Verify(m => m.Send(It.IsAny<SpecialReport>()), Times.Once());
}


* This source code was highlighted with Source Code Highlighter .


We start and make sure that the test fails. Now our efforts are focused on repairing this test. Here, designing as usual ends and we return to programming:

public int SendReports()
{
IList<Report> reports = reportBuilder.CreateRegularReports();

if (reports.Count == 0)
{
reportSender.Send(reportBuilder.CreateSpecialReport());
}

foreach (Report report in reports)
{
reportSender.Send(report);
}

return reports.Count;
}


* This source code was highlighted with Source Code Highlighter .


We run tests - all 3 tests pass. We implemented a new function and did not break the old ones. This is good news!

Question: How to find out what code has already been tested?
Code coverage tests can be verified using various utilities. For a start, I can advise PartCover .

Question: Is it necessary to strive to cover the code with tests for 100%?
Not. This requires too much effort to create such tests and even more to support them. Normal cover ranges from 50 to 90%. Those. all business logic should be covered without accessing the database, external services and the file system.


I propose to implement the second requirement for you and share in the comments the final part of the SendReports function and your test. You will write a test first, right?

Question: How can I test the interaction with the database, work with the SMTP server or the file system?
Indeed, it is necessary to test it. But this is not done by unit tests. Because the unit tests must pass quickly and not depend on external sources. Otherwise, you will run them once a week. For more details, see the article “Effective modular test” .

Q: When can I use TDD?
TDD can be used to create any application. It is very convenient to use it if you are studying the possibilities of a new library or programming language. There are no special limits in application. There may be inconvenience with testing multi-threaded and other specific applications.


Conclusion



I wish every developer to try this practice. After that, you can decide how TDD is suitable for you personally and for the project as a whole.

Source: https://habr.com/ru/post/82842/


All Articles