Replace test: Mock, Stub, and others. Replace test mockstub.
Introduction
Ideally, all the tests you perform are advanced tests for your actual code. For example, the UI test simulates the actual user input (Klaas is discussed in his article. In practice, this is not always a good idea. Accessing the database once for each test case or rotating the UI will slow your test, which will reduce your productivity and cause you to not run those tests frequently. If a code you test depends on a network connection, this requires that your test environment have network access conditions, and it is difficult to simulate some special tests, for example, when the phone is in flight mode.
For this reason, we can use some simulated code to replace your actual code to write some test cases.
When do you need some mock objects?
Let's start with the basic definitions of these different types of simulation objects.
DoubleIt can be understood as replacement, which is the collective name of all simulated test objects. We can also call it a replacement. Generally, when you create any test replacement object, it is used to replace the object of a specified class.
StubIt can be understood as a testing pile, which can return a specified simulated value when a specific method is called. If your test case requires an associated object to provide some data, you can use stub to replace the data source. During the test setting, you can specify to return consistent simulation data each time.
SpyIt can be understood as an investigation, which is responsible for reporting the situation, continuously tracking the method called, and the parameters passed in the call process. You can use it to implement test assertions, such as whether a specific method is called or whether a correct parameter is used. It is useful when you need to test some protocols or relationships between two objects.
MockIt is similar to spy, but it is somewhat different in use.SpyTracking all method calls and asking you to write assertions afterwards, mock usually requires you to set your expectations in advance. You tell it what you expect, then execute the test code and verify that the final result is consistent with the previously defined expectation.
FakeIt is an object with complete function implementation and behavior. In behavior, it is the same as the actual object of this type, but unlike the class it simulates, it makes testing easier. A typical example is to use a database in the memory to generate a data persistence object, instead of accessing a database in a real production environment.
In practice, these terms are often used differently from their definitions and can even be exchanged. Later, we will see some libraries in this article. They think they are "mock object frameworks", but they also provide stub functions, in addition, the verification behavior is similar to the "spy" I described rather than "mock ". So don't fall into the details of these terms too much. I define these terms more because they need to be differentiated at a high level and it will help to consider the behavior of different types of test objects.
If you are interested in more details about different types of simulated testing objects, Martin Fowler's article "Mocks Aren't Stubs" is considered an authoritative discussion on this issue.
Mockists vs Statists)
Many articles about simulated objects are derived from Fowler. they discuss the tests written by two different types of programmers, simulators and statisticians.
The idea is to test the interaction between objects. By using a simulated object, you can more easily verify whether the tested object complies with the protocols it has established with other classes, so that a correct external call occurs at the right time. For developers who use behavior-driven, this test can drive better production code, because you need to explicitly simulate specific methods, this helps you design more elegant APIs used between two objects. This idea is closely related to the simulation driver. Therefore, simulated tests prefer unit-level tests instead of end-to-end tests.
The statistical method is not to use simulated objects. This approach is to test the status rather than behavior during testing, so this type of testing is more robust. When using a simulated test, if you update the actual class behavior, the simulation class also needs to be updated synchronously. If you forget to do so, you may encounter situations where the test can pass but the Code cannot work correctly. By emphasizing that only the real code is used in the test environment, testing with statistical ideas can help you reduce the coupling between the test code and the implementation code, and reduce the error rate. This type of test, as you may have guessed, is suitable for more comprehensive end-to-end testing.
Of course, it doesn't mean that there are two opposing programmer schools; you can't see the confrontation between simulation and statistics. This kind of divergence is useful, but you must realize that mock is sometimes the best tool in your toolbox, but sometimes it is not. Different types of tests apply to different tasks, and the most efficient test suite is often a collection of different test styles. Think carefully about what you really want to use a single test to verify. This will help you find the most suitable test method and help you decide for the current job, use a tool to simulate whether the test object is correct.
In-depth code
In theory, everything is okay, but let's look at a real use case that you need to use mock.
Let's try to test an object. The above method is calledUIApplication
OfopenURL:
Method to open another application. (This is a real problem I encountered when testing my IntentKit library .) It is very difficult to write an end-to-end test for this case, even if it is possible, because the 'success status' itself causes the application to close. The natural choice is to simulateUIApplication
Object, and verify whether the simulated object is actually calledopenURL
Method to open the correct URL.
Suppose this object has the following method:
@interface AppLinker : NSObject - (instancetype)initWithApplication:(UIApplication *)application; - (void)doSomething:(NSURL *)url;@end
This is a very far-fetched example, but please tolerate me. In this example, you will notice that we use the constructor for injection. When we createAppLinker
WhenUIApplication
Object injection. In most cases, using a simulated object requires some form of dependency injection. If this concept is unfamiliar to you, please take a look at the description in Jon's article.
OCMockito
OCMockito is a lightweight library that uses simulated objects:
UIApplication *app = mock([UIApplication class]);AppLinker *linker = [AppLinker alloc] initWithApplication:app];NSURL *url = [NSURL urlWithString:@"https://google.com"];[linker doSomething:URL];[verify(app) openURL:url];
OCMock
OCMock is another Simulated Object Library of Objective-C. Like OCMockito, it provides all the features about stub and mock and includes all the features you may need. It is more powerful than OCMockito and depends on your personal choice. It has its own advantages and disadvantages.
At the most basic level, we can use OCMock to re-write a test that is very similar to the previous one:
id app = OCMClassMock([UIApplication class]);AppLinker *linker = [AppLinker alloc] initWithApplication:app];NSURL *url = [NSURL urlWithString:@"https://google.com"];[linker doSomething:url];OCMVerify([app openURL:url]);
This Simulated Test style that verifies the call method after testing is considered as a "post-run Verification" method. OCMock added support for this function only after the latest version 3.0. At the same time, it also supports the style of the old version, that is, to verify the expected run, set the expectations for the test results before executing the test code. Finally, you only need to verify whether the expectations correspond to the actual results:
id app = OCMClassMock([UIApplication class]);AppLinker *linker = [AppLinker alloc] initWithApplication:app];NSURL *url = [NSURL urlWithString:@"https://google.com"];OCMExpect([app openURL:url]);[linker doSomething:url];OCMVerifyAll();
Because OCMock lets you stub out class methods, you cocould also test this using OCMock, if your implementationdoSomething
Uses[UIApplication sharedApplication]
Rather thanUIApplication
Object injected in the initializer: Because OCMock also supports stub of class methods, you can also use this method to test. IfdoSomething
Method passed[UIApplication sharedApplication]
InsteadUIApplication
Object injection initialization:
id app = OCMClassMock([UIApplication class]);OCMStub([app sharedInstance]).andReturn(app);AppLinker *linker = [AppLinker alloc] init];NSURL *url = [NSURL urlWithString:@"https://google.com"];[linker doSomething:url];OCMVerify([app openURL:url]);
You will find that the stub class methods and stub instance methods look the same.
Build your own test
For simple use cases like this, you may not need such a heavyweight simulated object Test Library. Generally, you only need to create your own simulated object to test your actions:
@interface FakeApplication : NSObject @property (readwrite, nonatomic, strong) NSURL *lastOpenedURL; - (void)openURL:(NSURL *)url;@end@implementation FakeApplication - (void)openURL:(NSURL *)url { self.lastOpenedURL = url; }@end
The following is a test:
FakeApplication *app = [[FakeApplication alloc] init];AppLinker *linker = [AppLinker alloc] initWithApplication:app];NSURL *url = [NSURL urlWithString:@"https://google.com"];[linker doSomething:url];XCAssertEqual(app.lastOpenedURL, url, @"Did not open the expected URL");
This may happen for an example similar to the one already designed. creating your own simulated object only adds a lot of unnecessary samples, however, if you think you need to simulate more complex object interactions, it is very valuable to fully control the behavior of simulated objects.
Which one is used?
Which of the following solutions depends on your specific test conditions and your personal preferences. Both OCMockito and OCMock can be installed through CocoaPods to integrate them into your existing test environment. However, it is very easy to add other Dependencies unless you need them. In addition, it is best to create some simple simulated objects unless necessary.
Considerations during simulated testing
One of the biggest problems you may encounter in any form of testing is that the write test is too tightly coupled with the implementation code. One of the most important points in the test is to reduce the cost of future changes. If the implementation details of the changed code damage the current test, this cost has increased. That is to say, in order to minimize the adverse effects caused by the use of simulated testing, you have a lot to do.
Dependency injection is your good partner
If you have not used dependency injection, you may need it. Although it is also possible to simulate objects without dependency injection (for example, the OCMock simulation method is used above), it is usually impossible. Even if possible, the complexity introduced by the Setup test may be greater than the benefits it brings. If you use dependency injection, you will find it much easier to use stub and mock to write tests.
Do not simulate what you don't have
Many experienced testers warn you that "do not simulate what you don't have" means you should create mock or stub only for the objects in your code library, instead of creating third-party dependencies or libraries. There are two main reasons: one is based on the actual situation and the other is more philosophical.
For your code library, you may feel the stability and instability of different interfaces, therefore, you can use your intuition to determine if the replacement test method may lead to a vulnerability in the test. Generally, you are not sure about third-party code. To solve this problem, a common practice is to create a packaging class for third-party code to abstract its behavior. In some cases, it is meaningless to simply transfer complexity rather than reduce complexity. However, in some cases, you may frequently use your third-party code, which is a good method to streamline your testing. Your unit test can simulate a custom object and test your packaging class by using high-level integration or function tests.
The uniqueness of iOS and OS X development world makes things a little complicated. Many of our tasks depend on Apple's framework, which far surpasses some standard libraries in other languages. AlthoughNSUserDefaults
It is not a "you own" object, but if you find that you need to simulate it, do it with confidence, apple is unlikely to release a new version of Xcode to break this API.
Another reason not to simulate third-party dependent libraries is more philosophical. Part of the reason for writing a test in a simulated style is that such a test can easily find the clearest and feasible interface between two objects. However, if it is a third-party dependency, you cannot control it. some details of the API Protocol have been disabled by the third-party library, therefore, you cannot test the interface to verify whether there is room for improvement through the experiment. This is not a problem in itself, but in many cases, it reduces the simulation test effect until the advantages of the simulation test are wiped out.
Do not imitate me!
There is no silver bullet in the test; based on your personal preferences and the specific characteristics of the Code, different policies are required under different circumstances. Test substitutes may not work in all cases, but they will be a very effective tool in your test toolbox. Whether you prefer to use the Framework to simulate everything in unit tests or just create your own simulated objects as needed, it makes sense to remember the simulated objects when you think about how to test your code.