The art of Unit Test
- The second edition of “The Art of Unit Testing” with ISBN code 978-1617290893 has the following chapters:
- The basics of unit testing
- A first unit test
- Using stubs to break dependencies
- Interaction testing using mock objects
- Isolation (mocking) frameworks
- Digging deeper into isolation frameworks
- Test hierarchies and organization
- The pillars of good unit tests
- Integrating unit testing into the organization
- Working with legacy code
- Design and testability123.
Chapter1 The basics of Unit Testing
Definition
A unit of work is the sum of actions that take place between the invocation of a public
method in the system and a single noticeable end result by a test of that system. A
noticeable end result can be observed without looking at the internal state of the system and only through its public APIs and behavior. An end result is any of the following:
■ The invoked public method returns a value (a function that’s not void).
■ There’s a noticeable change to the state or behavior of the system before and
after invocation that can be determined without interrogating private state.
(Examples: the system can log in a previously nonexistent user, or the system’s
properties change if the system is a state machine.)
■ There’s a callout to a third-party system over which the test has no control, and
that third-party system doesn’t return any value, or any return value from that
system is ignored. (Example: calling a third-party logging system that was not
written by you and you don’t have the source to.
一个单元测试的最终结果,有三种类型。返回值,系统状态变化,调用第三方系统。
测试方法名称三部分
UnitOfWorkName
Scenario【ByDefault,WhenCalled,Always】
ExpectedBehavior
例子:IsValidFileName_BadExtension_ReturnFalse()
NUnit need at least two test attribute
TestFixture
Test
Three main actions which comprise a unit test
A unit test typically consists of the following three main actions:
-
Arranging the Test Preconditions: This involves setting up the necessary objects, data, and conditions required to run the test. This can involve creating objects, initializing variables, and preparing any resources required for the test.
-
Act or Exercise the Unit Under Test: This involves running the code or method that is being tested. The test should check that the code behaves as expected and that it produces the correct results.
-
Asserting the Results: This involves checking the output or result of the code that was run and verifying that it meets the expected results. This can be done using assertion methods or libraries provided by the testing framework.
These three actions, arrange-act-assert, are the key components of a unit test. By following this pattern, developers can ensure that their tests are well-structured, isolated, and repeatable, and that they accurately reflect the expected behavior of the code under test.
第八章
关于实践可读性目标的内容
讨论更多提高代码可维护性的技术
避免使用setup初始化实例,可以使用factory method来处理,参见第7章
TearDown基本不在单元测试中被使用,除非是为了重置静态变量或者单例的状态。其他的使用,都在集成测试中。
2.6.2 checking for expected exceptions
使用NUnit提供的Assert.Catch方法来处理
What's Assert.That in NUnit? explain the parameter
Assert.That
is an assertion method in NUnit that allows you to specify the expected outcome of a test in a flexible and powerful way. It takes two parameters: the first parameter is the actual value being tested, and the second parameter is a constraint that specifies the expected outcome.
The second parameter is an instance of the Constraint
class, which can be one of many built-in NUnit constraints, or a custom constraint that you define yourself. These constraints allow you to define a wide range of tests, from simple equality tests to more complex checks on the structure or behavior of an object.
Here are some examples of how you can use Assert.That
with different constraints:
-
Equality test:
Assert.That(result, Is.EqualTo(expectedResult));
-
Comparison test:
Assert.That(result, Is.GreaterThan(10));
-
Type test:
Assert.That(result, Is.TypeOf<int>());
-
Collection test:
Assert.That(list, Has.Exactly(3).Items);
-
String test:
Assert.That(text, Does.StartWith("Hello").And.EndWith("world"));
By using Assert.That
, you can write tests that are more expressive, easier to read, and more maintainable than traditional NUnit assertions that use Assert.AreEqual
or similar methods. Additionally, the constraint-based approach offered by Assert.That
allows you to create complex tests that can validate multiple aspects of the system under test, making it a powerful tool in the NUnit testing framework.
2.6.5 设置Category, 比如快测试和慢测试
2.7 Testing results that are system state changes instead of return values 测试系统状态的改变而非返回值
checking that the system's behavior is different after performing an action on the system under test
Having convered the basics in previous chapters, I'll now introduce the core testing and refactoring techniques necessary tests in the real world.
In Chapter3, we'll begin by examing stubs and how they help break dependencies. We'll go over refactoring techniques that make code more testable, and you'll learn about seams in the process.
In Chapter4, we'll move on to mock objects and interaction testing and look at how mock objects differ from stubs, and we'll explore the concept of fakes.
In Chapter5, we'll look at isolation frameworks(also known as mocking frameworks) and how they solve some the repetitive coding involved in handwirtten mocks and stubs.
Chapter 6 also compares the leading isolation frameworks in .Net and use FakeItEasy for examples, showing its API in common use cases.
The concept of stubs in NUnit, and with C# sample code
Stubs are test-specific objects that simulate the behavior of real objects for testing purposes. In NUnit, stubs are used to isolate the code being tested and to control the behavior of dependencies that the code interacts with.
For example, consider a class that depends on an external database to perform some operation. In a unit test, you might use a stub to simulate the database so that you can control the data being returned and test the class's behavior without actually having to hit a real database.
Here is an example of a stub implementation in C# using NUnit:
public interface IDatabase
{
int GetData();
}
public class DatabaseStub : IDatabase
{
private readonly int _data;
public DatabaseStub(int data)
{
_data = data;
}
public int GetData()
{
return _data;
}
}
[TestFixture]
public class ClassBeingTestedTests
{
[Test]
public void TestMethod()
{
// Arrange
var databaseStub = new DatabaseStub(42);
var classBeingTested = new ClassBeingTested(databaseStub);
// Act
int result = classBeingTested.DoSomething();
// Assert
Assert.AreEqual(42, result);
}
}
In this example, the ClassBeingTested
class depends on an instance of IDatabase
to perform some operation. The DatabaseStub
class implements IDatabase
and provides a hardcoded return value for GetData()
. In the test method, an instance of DatabaseStub
is created and passed to an instance of ClassBeingTested
. This allows the test to control the behavior of the database without hitting a real database.
第三章 Using stubs to break dependencies
什么情况下使用stub
In this chapter, we’ll take a look at more realistic examples where the object
under test relies on another object over which you have no control (or that doesn’t
work yet).
That object could be a web service, the time of day, threading, or many
other things. The important point is that your test can’t control what that dependency returns to your code under test or how it behaves (if you wanted to simulate
an exception, for example). That’s when you use stubs
In chapter 4 we will have an expanded definition of stubs, mocks, and fakes and how
they relate to each other. For now, the main thing to remember about mocks versus
stubs is that mocks are just like stubs, but you assert against the mock object, whereas
you do not assert against a stub
For the sake of simplicity, let’s assume
that the allowed filenames are stored somewhere on disk as a configuration setting for
the application, and that the IsValidLogFileName method looks like this:
LogAnalyzer类的IsValidLogFileName直接读取文件系统上的配置文件,来决定日志文件名是否有效。
public bool IsValidLogFileName(string fileName)
{
//read through the configuration file
//return true if configuration says extension is supported.
}
Figure 3.1 Your method has a direct dependency on the
filesystem. The design of the object model under test inhibits you
from testing it as a unit test; it promotes integration testing
Transferring this pattern to your code requires more steps:
1 Find the interface that the start of the unit of work under test works against. (In
this case, “interface” isn’t used in the pure object-oriented sense; it refers to the
defined method or class being collaborated with.) In our LogAn project, this is
the filesystem configuration file.
2 If the interface is directly connected to your unit of work under test (as in this
case—you’re calling directly into the filesystem), make the code testable by adding a level of indirection hiding the interface. In our example, moving the direct
call to the filesystem to a separate class (such as FileExtensionManager) would
be one way to add a level of indirection. We’ll also look at others. (Figure 3.3
shows how the design might look after this step.)
3 Replace the underlying implementation of that interactive interface with something that you have control over. In this case, you’ll replace the instance of the
class that your method calls (FileExtensionManager) with a stub class that you
can control (StubExtensionManager), giving your test code control over external dependencies.
Your replacement instance will not talk to the filesystem at all, which breaks the
dependency on the filesystem. Because you aren’t testing the class that talks to the filesystem but the code that calls this class, it’s OK if that stub class doesn’t do anything
but make happy noises when running inside the test. Figure 3.4 shows the design
after this alteration
Figure3.3 Introducing a layer of indirection to avoid a direct dependency on the filesystem.The code that calls the filesystem is separated into a FileExtensionManager class, which will later be replaced with a stub in your test
IsValidLogFileName之前是直接去访问文件系统的话,在进行抽象之后,把访问文件系统的功能抽象成FileExtensionManager。
然后在测试的时候,就可以用Stub来替换FileExtensionManager。
3.4 Refactoring your design to be more testable
It’s time to introduce two new terms that will be used throughout the book: refactoring
and seams.
Figure 3.4 Introducing a stub to break the dependency. Now your class shouldn’t
know or care which implementation of an extension manager it’s working with.
In figure 3.4, I’ve added a new C# interface into the mix. This new interface will allow the object model to abstract away the operations of what a FileExtensionManager
class does,
and it will allow the test to create a stub that looks like a FileExtensionManager.
You’ll see more on this method in the next section.
We’ve looked at one way of introducing testability into your code base—by creating a new interface. Now let’s look at the idea of code refactoring and introducing seams
into your code
DEFINITION Refactoring is the act of changing code without changing the code’s
functionality. That is, it does exactly the same job as it did before. No more and
no less. It just looks different. A refactoring example might be renaming a
method and breaking a long method into several smaller methods.
DEFINITION Seams are places in your code where you can plug in different
functionality, such as stub classes,
adding a constructor parameter,
adding a public settable property,
making a method virtual so it can be overridden,
or externalizing a delegate as a parameter or property so that it can be set from outside a class.
Seams are what you get by implementing the Open-Closed
Principle, where a class’s functionality is open for extenuation, but its source
code is closed for direct modification. (See Working Effectively with Legacy Code
by Michael Feathers, for more about seams, or Clean Code by Robert Martin
about the Open-Closed Principle.
To break the dependency between your code under test and the filesystem, you
can introduce one or more seams into the code. You just need to make sure that the
resulting code does exactly the same thing it did before. There are two types of
dependency-breaking refactorings, and one depends on the other. I call them Type A
and Type B refactorings:
■ Type A—Abstracting concrete objects into interfaces or delegates
■ Type B—Refactoring to allow injection of fake implementations of those delegates
or interfaces
In the following list, only the first item is a Type A refactoring. The rest are Type B
refactorings:
■ Type A—Extract an interface to allow replacing underlying implementation.
■ Type B—Inject stub implementation into a class under test.
■ Type B—Inject a fake at the constructor level
■ Type B—Inject a fake as a property get or set.
■ Type B—Inject a fake just before a method call
3.4.2 Dependency injection: inject a fake implementation into a unit
under test
There are several proven ways to create interface-based seams in your code—places
where you can inject an implementation of an interface into a class to be used in its
methods. Here are some of the most notable ways:
■ Receive an interface at the constructor level and save it in a field for later use.
■ Receive an interface as a property get or set and save it in a field for later use.
■ Receive an interface just before the call in the method under test using one of
the following:
– A parameter to the method (parameter injection)
– A factory class
– A local factory method
– Variations on the preceding techniques
The parameter injection method is trivial: you send in an instance of a (fake) dependency to the method in question by adding a parameter to the method signature.
Let’s go through the rest of the possible solutions one by one and see why you’d
want to use each
3.4.3 Inject a fake at the constructor level (constructor injection)
在第4章,我们会扩展存根,模拟对象,以及伪对象的定义,并讨论它们之间的关系。
关于mock和stub的区别,现在只需要记住一个主要事实,mock和stub很类似,但是你会对mock进行断言,而不会对stub进行断言。
Chapter 4 Interaction testing using mock objects 使用mock对象进行交互测试
In the previous chapter, you solved the problem of testing code that depends on
other objects to run correctly. You used stubs to make sure that the code under test
received all the inputs it needed so that you could test its logic independently.
Also, so far, you’ve only written tests that work against the first two of the three
types of end results a unit of work can have: returning a value and changing the
state of the system.
In this chapter, we’ll look at how you test the third type of end result—a call to
a third-party object.
You’ll check whether an object calls other objects correctly.
The object being called may not return any result or save any state, but it has
complex logic that needs to result in correct calls to other objects that aren’t
under your control or aren’t part of the unit of work under test.
Now, back to the irrigation system. What is that device that records the irrigation
information? It’s a fake water hose, a stub, you could say. But it’s a smarter breed of
stub—a stub that records the calls made to it, and you use it to define if your test
passed or not. That’s partly what a mock object is. The clock that you replace with a
fake clock? That’s a stub, because it just makes happy noises and simulates time so that
you can test a different part of the system more comfortably.
DEFINITION A mock object is a fake object in the system that decides whether
the unit test has passed or failed. It does so by verifying whether the object
under test called the fake object as expected. There’s usually no more than
one mock per test
A mock object may not sound much different from a stub, but the differences are
large enough to warrant discussion and special syntax in various frameworks, as you’ll
see in chapter 5. Let’s look at exactly what the differences are.
Now that I’ve covered the idea of fakes, mocks, and stubs, it’s time for a formal definition of the concept of fakes
DEFINITION A fake is a generic term that can be used to describe either a stub
or a mock object (handwritten or otherwise), because they both look like the
real object. Whether a fake is a stub or a mock depends on how it’s used in
the current test. If it’s used to check an interaction (asserted against), it’s a
mock object. Otherwise, it’s a stub.
Let’s dive more deeply to see the distinction between the two types of fakes.
The basic difference is that stubs can’t fail tests. Mocks can
The easiest way to tell you’re dealing with a stub is to notice that the stub can never
fail the test. The asserts that the test uses are always against the class under test.
On the other hand, the test will use a mock object to verify whether or not the test
failed. Figure 4.2 shows the interaction between a test and a mock object. Notice that
the assert is performed on the mock
Again, the mock object is the object you use to see if the test failed or not. Let’s look at
these ideas in action by building your own mock object
4.3 A simple handwritten mock example
Creating and using a mock object is much like using a stub, except that a mock will do
a little more than a stub: it will save the history of communication, which will later be
verified in the form of expectations.
Let’s add a new requirement to your LogAnalyzer class. This time, it will have to
interact with an external web service that will receive an error message whenever the
LogAnalyzer encounters a filename whose length is too short.
Unfortunately, the web service you’d like to test against is still not fully functional,
and even if it were, it would take too long to use it as part of your tests. Because of that,
you’ll refactor your design and create a new interface for which you can later create a
mock object. The interface will have the methods you’ll need to call on your web service and nothing else.
Figure 4.3 shows how your mock, implemented as FakeWebService, will fit into
the test
4.4 Using a mock and a stub together
Let’s consider a more elaborate problem. This time LogAnalyzer not only needs to
talk to a web service, but if the web service throws an error, LogAnalyzer has to log the
error to a different external dependency, sending it by email to the web service
administrator, as shown in figure 4.4
Notice that there’s logic here that only applies to interacting with external objects;
there’s no value being returned or system state changed. How do you test that LogAnalyzer calls the email service correctly when the web service throws an exception?
Here are the questions you’re faced with:
■ How can you replace the web service?
■ How can you simulate an exception from the web service so that you can test
the call to the email service?
■ How will you know that the email service was called correctly, or at all?
You can deal with the first two questions by using a stub for the web service. To solve
the third problem, you can use a mock object for the email service.
In your test, you’ll have two fakes. One will be the email service mock, which you’ll
use to verify that the correct parameters were sent to the email service. The other will
be a stub that you’ll use to simulate an exception thrown from the web service. It’s a
stub because you won’t be using the web service fake to verify the test result, only to
make sure the test runs correctly. The email service is a mock because you’ll assert
against it that it was called correctly. Figure 4.5 shows this visually
第10章,会进行更多关于遗留代码的讨论。
可读性是编写单元测试需要关注的一个重要方面,我们会在第8章中进行讨论。
第4章 Interaction testing using mock objects使用模拟对象进行交互测试
作者:Chuck Lu GitHub |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2019-01-30 Decorator pattern
2019-01-30 JSON and XML Serialization in ASP.NET Web API
2019-01-30 Media Formatters in ASP.NET Web API 2
2015-01-30 C#高级编程(第9版) -C#5.0&.Net4.5.1 书上的示例代码下载链接