One of the challenges developers face when writing unit tests is how to handle external dependencies. In order to run a test, you may need a connection to a fully populated database, or some remote server; or perhaps there is a need to instantiate a complex class created by someone else.
开发人员在写单元测试时遇到的一个挑战就是如何处理外部的依赖关系。为了运行一个test,你可能需要链接到一个数据库或者远程server或者可能需要实例化一个复杂的类。
All these dependencies hinder the ability to write unit tests. When such dependencies need a complex setup for the automated test to run, the end result is fragile tests that break, even if the code under test works perfectly.
所有这些依赖阻碍了写单元测试的能力。当这些依赖需要一个复杂的配置才可以运行的时候,最终的结果是脆弱的即使test正常运行。
This article will cover the subject of mocks (also known as test doubles, stubs and fakes, amongst other names) and creating manual mocks vs. using a full-fledged mocking framework.
本文将囊括mocks的主题(test doubles, stubs and fakes, amongst other names)并且实用mocking framework来创建手动mocks。
But first - What are unit tests?
Unit tests are short, quick, automated tests that make sure a specific part of your program works. They are performed by testing a specific functionality of a method or class which has a clear pass/fail condition. By writing unit tests, developers can make sure their code works, before passing it to QA for further testing.
单元测试是简短快速的自动化测试来确认程序的特定部分运行正常。通过一个有明确pass/fail条件的特定的方法或者类来实现。通过写单元测试,开发者可以确认在送测qa进行进一步测试之前他们的代码运行正常。
Let’s look at a C# class with a method we’d like to test.
public class UserService { public bool CheckPassword(string userName, string password) { ... } }
We can then write a test that checks for a valid user and password, the method it returns true:
[Test] public void CheckPassword_ValidUserAndPassword_ReturnTrue() { UserService classUnderTest = new UserService(); bool result = classUnderTest.CheckPassword("user", "pass"); Assert.IsTrue(result); }
By writing several unit tests, we can test for various inputs to CheckPassword, we get expected results, and make sure it performs according to specifications.
Good unit tests run quickly and are isolated from other tests. Both these traits are difficult to achieve out of the box.
Writing unit tests: The obstacles
After a few days of writing tests, every developer hits a wall. The problem is that not all code is created equal and not all methods are simple to test. A common case: What happens when the method-under-test requires access to a database, to check if a user entry exists?
问题是并不是所有的方法测试起来都是方便简单的。一个常见的问题是:当方法需要访问一个数据库来检查user entry是否存在时如何测试?
Yes, we can set up a database before running the test however it would cost us precious time in which the developer is waiting for his ~1000 tests to run instead of writing code. Would you set up a database specific for every test and then clean it up after the test runs? We wouldn’t.
我们可以在运行测试前配置一个数据库,尽管如此但是这可能会花费我们大量的时间。在测试时你会配置特定数据库并且清除它吗?不会。
Sometimes, the code-under-test works perfectly but then you need to set up the environment that the tests depend on. This dependency makes your tests very brittle and they could fail. If tests sometimes fail, depending on the environment, you won’t trust them, which defeats the point.
有时test中的代码运行正常但是你却需要配置测试依赖的环境。这个依赖使得你的测试非常脆弱并且这些测试可能会失败。如果有时测试失败了,依赖于一定的环境,你就不会在信任他们。
The key to solving these issues lies in the usage of mocking. The mocking mechanism replaces the production code behavior (like the database calls), with code that we can control.
解决这些问题的关键是实用mocking。mocking机制代替了业务代码行为(比如数据库调用),而是实用我们可以控制的代码。
Hand-Rolled Mocking
The first mocking experience usually starts out with hand-rolled mocks. These are classes that are coded by the developer and can replace the production objects for the purpose of testing.
第一个mocking经验通常是hand-rolled mocks。这些类由开发者编写并且可以替换测试目的的production objects。
Creating your very first mock is simple - all you need is a class that looks like the class you're replacing, which can be done using inheritance (in Object Oriented languages).
创建mock很简单-所有你需要的就是一个你想要替换的类。而这些类可以被继承
Here is the previous example- now with the CheckPassword method fully implemented:
public class UserService { private IDataAccess_dataAccess; public UserService(IDataAccess dataAccess) { _dataAccess = dataAccess; } public bool CheckPassword(string userName, string password) { User user = _dataAccess.GetUser(userName); if (user != null) {if (user.VerifyPassword(password)) { return true; } } return false; } }
Our production code uses external data (Database) in order to store the application's users. Because deploying and populating a database would increase the test's run time and might fail some other solution is in order.
为了存储用户,我们的业务代码使用了外部数据(Database)因为部署和生成数据库会增加测试的运行时间并且可能使得一些顺序执行的方案失败。
In order to test the CheckPassword method we need to provide an object that would implement IDataAccess, but that we control. Doing so is quite simple: all we need to do is to create a new class that will implement the desired interface, only whilst the real DataAccess class’ implementation goes to the database, ours just returns a value we supply:
为了测试CheckPassword方法我们需要提供一个对象可以执行IDataAccess。这样做非常简单:我们需要做的只是创建一个新类来执行想要的接口。同时执行真实DataAccess类对数据库的操作。仅仅返回一个我们需要的值。username
public class DummyDataAccess : IDataAccess
{
private User _returnedUser;
public DummyDataAccess(User user)
{
_returnedUser = user;
}
public User GetUser(string userName)
{
return _returnedUser;
}
}
The class we've created does just one thing - it returns a User when called, instead of calling the database. Using this DummyDataAccess we're now able to alter our test to make it pass:
我们创建的类仅仅做了一件事-当被调用的时候返回一个user而不是调用数据库。使用DummyDataAccess 我们现在就可以提醒我们的test使其通过。
[Test] public void CheckPassword_ValidUserAndPassword_ReturnTrue() {
//创建user实例 User userForTest = new User("user", "pass");
//将user实例传递给fake类 IDataAccess fakeDataAccess = new DummyDataAccess(userForTest);
//传递fakeDataAccess给UserService
UserService classUnderTest = new UserService(fakeDataAccess);
//调用接口
bool result = classUnderTest.CheckPassword("user", "pass");
//判断值
Assert.IsTrue(result);
}
First we create a User and a DummyDataAccess object that would return that User. Then we create a real UserService(the class we want to test), and supply it with the DummyDataAccess. We then call the method under test, which eventually uses our fake implementation to return the supplied User data.
首先我们创建了user并且一个DummyDataAccess对象会返回user。然后我们创建真实的UserService(我们想测试的类)然后提供一个DummyDataAccess。然后我们在test下调用这个方法,实际上是用我们的fake假操作来返回需要的user数据。
At first this might look like cheating - it seems that the test does not use the production code fully, and therefore does not really test the system properly. But remember: we're not interested in the working of the data access in this particular unit test; all we want to test is the business logic of the UserService class.
首先这个看起来是富有欺骗性的-看起来测试完全没有使用业务代码并且因此不会真正测试系统属性。但是记住:我们并不关心在这个单元测试中关于访问数据的操作,我们仅仅想测试userservice类的业务逻辑。
We should also have additional tests; these ones test that we read and write data correctly into our database. These are called integration tests
我们应当也有其他的测试,这些测试我们正确的读取和写入数据库。这些测试被成为集合测试。
Using the DummyDataAccess class achieves three goals:
使用DummyDataAccess达到了三个目的:
So what is all this mocking about?
While you can spend the whole day categorizing and naming different sorts, we’ll just mention the classic mocks and stubs.
You’ll see that people give their own definitions using the same words. Because of the loaded definitions of the two, from this point on we’ll call both "fake objects" (or "fakes" for short). While the difference exists, it becomes less apparent in modern mocking frameworks, and just not worth the fuss.
Why not stop at hand-rolled fakes?
At first, using manually written fake objects seems like a good idea. All software developers know how to write code, and implementing an interface or deriving a new class is a no-brainer.
刚开始,使用手写fake对象看起来是个好主意。所有的软件开发人员都知道如何写code并且执行一个接口或者派生一个新的类。
The problem is that the simple fake class created yesterday, becomes a maintenance nightmare today. Here are a couple of reasons why:
问题是简单的假类有许多问题:
lets go back to the example from the beginning of this article. What happens when we add a new method to theIDataAccess interface? We now need to also implement the new method in the fake object (usually we’ll have more than one, so we’ll need to implement in the other fakes too). As the interface grows, the fake object is forced to add more and more methods that are not really needed for a particular test just so the code will compile. That’s usually necessary work, with almost no value.
虽然接口的增长,fake对象也被迫添加越来越多的方法但是却不是真的被特定test所需要。比如现在只有getuser,那么后面如果有需要的话可能会需要更多的方法。这些是必须的但是却咩有任何价值。
one way around the method limitation is to create a real class and derive the fake object from it, only faking the methods needed for the tests to pass. Sometimes it can prove risky, though.
绕过方法限制的一个途径是创建一个真实的类并且fake 对象派生自这个类,只是faking能让test通过的方法。有时候它可能是有危险的。
The problem is that once derived, our fake objects have fields and methods that perform real actions and could cause problems in our tests. A recent example we encountered shows why: A hand rolled fake was inherited from a production class: it had an internal object opening a TCP connection upon creation. This caused very strange failures in my unit tests, until we were able to track it down. In this case, we wasted time because of the way we created the fake object.
问题是一旦派生,我们的fake对象们就有fields和方法来执行真实的操作并且可能会对我们的测试产生问题。例如:a手动创建了fake继承于一个production class:class有一个内部的对象开启了一个tcp连接。这个在单元测试的时候导致了非常奇怪的失败,正是由于创建fake假对象的方式导致了这个问题使得花费了很长的时间。
as the number of tests increases, we’ll be adding more functionality to our fake object. For some tests method X returns null, while for other tests it returns a specific object. As the needs of the tests grow and become distinct, our fake object adds more and more functionality until it becomes so complicated that it may need unit testing of its own.
随着测试数量的增加,我们将添加更具有功能性的方法到我们的fake假对象中去。一些测试方法x返回null,而其他一些测试返回一个特定的object。随着测试的增长和变得有区别的,我们的fake假对象添加越来越多的功能直到他变得复杂到需要它自己的单元测试。
All of these problems require us to look for a more robust, industry grade solution - namely a mocking framework.
应运而生的就是mocking framework。
Mocking Frameworks
A mocking framework (or isolation framework) is a 3rd party library, which is a time saver. In fact, comparing the saving in code lines between using a mocking framework and writing hand rolled mocks, for the same code, can go up to 90%! Instead of creating our fake objects by hand, we can use the framework to create them, with a few API calls. Each mocking framework has a set of APIs for creating and using fake objects, without the user needing to maintain irrelevant details of the specific test - in other words, if a fake is created for a specific class, when that class adds a new method nothing needs to change in the test. One final remark: a mocking framework is just like any other piece of code and does not "care" which unit testing framework is used to write the test it's in.
mocking framework是一个第三方的库。你不必手动创建你的fake假对象,我们可以使用mocking framework来创建他们,仅仅需要调用几个api就可以了。每一个mocking framework都有一套api用来创建和使用fake假对象。换句话说,如果一个fake假对象为一个特定的类创建了,当这个类添加了一个新的方法时,test不需要有变动。最后一点:mocking framework就像任何其他的代码片段并且不关心哪个单元测试framework使用它。
Mocking framework types
Different frameworks work in different ways - some create a fake object at run-time, others generate the needed code during compilation, and yet others use method interception to catch calls to real objects and replace these with calls to a fake object. Obviously the framework’s technology dictates its functionality.
For example: if a specific framework works by creating new objects at run-time using inheritance, then that framework cannot fake static methods and objects that cannot be derived. It's important to understand the differences between frameworks, prior to committing to one. Once you build a large amount of tests, replacing a mocking framework can be expensive.
What can a mocking framework do for me?
Mocking frameworks perform three main functions:
mocking frameworks 执行三个主要的功能:
In the next examples, we've used Typemock Isolator, a .NET mocking framework. Wikipedia has an extensive list of mocking frameworks sorted by programming language at http://en.wikipedia.org/wiki/List_of_mock_object_frameworks.
1. Creating fake objects
Once a fake object is created, how does it behave? It might return a fake object, or throw an exception when called, depending on the mocking framework used. Most of the time, to use the new fake object in a test, additional code is required to set its behaviors.
一旦fake 对象被创建,它如何使用?它可能返回一个fake object或者抛出异常,取决于mocking framework的使用。大多数情况下,为了在test中使用新的fake对象,需要设置它的vehaviors代码。
2. Setting behavior on fake objects
After creating a fake object, its behavior needs to be configured. The following example takes the code from the beginning of this article and uses a mocking framework to create a set behavior of a fake DataAccess object:
创建了一个fake对象后,它的vehavior需要被配置。下面的例子就使用了mocking framework来创建dataaccess假对象
[Test]
public void CheckPassword_ValidUserAndPassword_ReturnTrue()
{
//user类实例化
User userForTest = new User("user", "pass");
IDataAccess fakeDA = Isolate.Fake.Instance<IDataAccess>();
Isolate.WhenCalled(() => fakeDA.GetUser(string.Empty)).WillReturn(userForTest)
UserService classUnderTest = new UserService(fakeDataAccess);
bool result = classUnderTest.CheckPassword("user", "pass");
Assert.IsTrue(result);
}
The test is very similar to the test we had before. This time, we don't need to create and maintain a class to fake theDataAccess class. We can create tests without worrying about any maintenance penalty.
3. Verify methods were called
Test frameworks can test state value: fields, properties, variables. Comparing the actual value to the expected ones are the pass/fail criteria. Mocking frameworks add another way to test - checking whether methods were called, in which order, how many times and with which arguments. For example, let’s say that a new test is required for adding a new user: if that user does not exist than call IDataAccess.AddNewUser method. Note that the AddUser method doesn’t have a return value we can check on. We need to know if the call actually happened.
测试框架可以测试状态值:fields,properties,variables。mocking frameworks添加了另外一个方式来测试-检查方法是否都被调用了。多少次和使用什么参数。例如,一个新的test需要添加一个新的user:如果user不存在那么就调用IDataAccess.AddNewUser 方法。注意adduser方法没有我们可以用来check的返回值。我们需要直到调用是否真的发生了。
[Test]
public void AddUser_UserDoesNotExist_AddNewUser()
{
IDataAccess fakeDA = Isolate.Fake.Instance<IDataAccess>();
Isolate.WhenCalled(() => fakeDA.GetUser(string.Empty)).WillReturn(null);
UserService classUnderTest = new UserService(fakeDA);
classUnderTest.AddUser("user", "pass");
Isolate.Verify.WasCalledWithAnyArguments(() => fakeDA.AddUser(null));
}
The test is very similar to the previous test, except for two points:
Some mocking frameworks have additional capabilities, other than the basic main three such as the ability to invoke events on the fake object or cause the creation of a specific fake object inside the product code. The three basic capabilities are the core functionality expected from every mocking framework. Additional features should be compared and checked when deciding which mocking framework to use.
Choosing a mocking framework
Changing an existing mocking framework requires updating all existing unit tests and so choosing the right mocking framework is important. There are no clear rules to how one should decide which framework to use but there are some key factors that need to be taken into consideration:
Not all mocking framework are created equal. Some frameworks might offer additional capabilities that other do - for example if a mocking framework uses inheritance to create fake objects it cannot fake static and non-virtual methods, while a framework that employs instrumentation and/or method interception can. Look for the features that are not strictly "mocking" features such as event and method invocation - those can help write better unit tests faster. Almost every mocking framework has a site or white paper detailing its features. Compare several frameworks features side by side and see which scores higher.
Just like any other 3rd party library it is important that a mocking framework should be easy to use. Try several frameworks to see which makes most sense to you. A simple, discoverable and readable API (application programming interface) is important because it would directly affect how readable the tests are that use it. Easy to use as well as easy to read are definitely factors worthy of consideration.
Some mocking frameworks are free while others cost money. Usually (but not always) there is a good reason that a certain framework is not given for free. Users might be entitled to premium support or training that would help getting up to speed with the new tool. The paid version might have features that the free frameworks do not have. Check the licensing scheme, keep in mind that you might need to purchase a new license for each developer in the team as well as build servers. At the end of the day it's all about ROI (return on investment) even a pricey tool that saves 50% of the time of each developer is worth the investment.
Best Practices and common pitfalls
When using a mocking solution it's easy to forget that it's merely a tool - and as such can be abused. Just like any other development tool it is up to the developer to learn to use it well.
Here are a couple of aspects you should know how to handle.
The key advice when using a fake object is to understand what is under test and what is the dependency. The object under test would not usually be faked - so finding the target and scope of the test helps finding out what to fake.
Using too many fake objects creates fragile tests - tests that are likely to break when production code changes occur. It is advisable to fake as little as possible. Faking chatty interfaces should be avoided because a small change in the order of calls would break your test. Start by writing the test without the fake objects then fake only what you need to makethe test pass.
Fake the objects directly called by the subject of the test. Unless you want to test the interaction between several classes try to limit the scope of the fake object to those directly affecting the class under test.
When you have a fake object, the entire world looks like it should be verified. It's easy to fall to the trap of making sure that method "A" calls method "B" - most of the time it does not really matter. Methods are refactored and changed frequently, so Verify should only be used where it's the only pass/fail criterion of your test.
When a test has more than one assert it may be testing too many things at the same time. The same principle applies to using Verify. Testing if three different methods where called should be done in three separate tests. If the first verify that fails throws an exception, we don’t have any clear knowledge about the success or failure of the other two. This keeps us further from fixing the problem.
Summary
Unit testing is a major component of every agile methodology. The early feedback you receive from your tests help you feel confident that you didn’t introduce new bugs, and that you gave QA actual working code.
This article was written to give you an understanding into the world and art of Mocking.
Mocking frameworks are essential tools for writing unit tests. In fact, without a tool like this, you’re bound to fail in your effort - either you won’t have unit tests that give early feedback, or no tests at all.
This is why deciding on a mocking framework or some other similar solution is as important as deciding the unit testing framework used. Once you pick a framework, master it. It helps make your unit testing experience easy and successful.
More Software Testing Knowledge
Software Quality Assurance Portal
Software Testing Videos and Tutorials