Recently, I started to learn about unit testing, mainly by learning PHPUnit. At present, I only followed the PHPunit manual and an entry-level tutorial to get started with PHPUnit and practice. I just had a simple understanding and did not have any in-depth research. Cool shell author has the following point of view: software development is a kind of mental work, is... recently, I started to learn about unit testing, mainly by learning PHPUnit. At present, I only followed the PHPunit manual and an entry-level tutorial to get started with PHPUnit and practice. I just had a simple understanding and did not have any in-depth research.
Cool shell author has the following points:
Software development is a kind of mental labor and knowledge-intensive work. Just like art works, there is no standard answer to the creation process and finished product.
The quality of software is not tested, but designed and maintained. It's just like the craftsman carved their work.
I hope you can share your or your team's views on unit testing and Their Applications in actual projects.
For example:
- Timing of unit test writing [After the software development is complete]
- Write unit test roles [Written by developers or non-developers]
- ...
Reply content:
Recently, I started to learn about unit testing, mainly by learning PHPUnit. At present, I only followed the PHPunit manual and an entry-level tutorial to get started with PHPUnit and practice. I just had a simple understanding and did not have any in-depth research.
Cool shell author has the following points:
Software development is a kind of mental labor and knowledge-intensive work. Just like art works, there is no standard answer to the creation process and finished product.
The quality of software is not tested, but designed and maintained. It's just like the craftsman carved their work.
I hope you can share your or your team's views on unit testing and Their Applications in actual projects.
For example:
- Timing of unit test writing [After the software development is complete]
- Write unit test roles [Written by developers or non-developers]
- ...
If I only answer the question, my personal feeling is one sentence:
The role of unit testing is ultimately to protect and even improve developers' self-confidence in the Code (whether it is your code or the code written by other members of the team ), and whether it is testing code, writing new code, or restructuring old code, this (to protect and improve self-confidence) is true.
If the above is tl or dr, I will write some personal experiences on the above points.
Correct understanding
Unit
Test
Unit testing is a subclass of testing. It is not a unit test if you write a test. Even if you use a unit test framework, you may write code that crosses the unit test boundary. The correct unit test is to ensure that the test code is accurately isolated (isolate) from the code to be tested. If you test a class, in the test code, you should avoid dependency on other classes (except the standard library of languages or the tool methods/helper methods provided by the Framework ), even if you test a method of this class, try to avoid dependency on other members of the class.
Of course, this is impossible in reality. 100% does not depend on any code without coupling, and it is useless even if it exists. We must inevitably make code interact with each other. Such interaction must also be reflected in the test code. I will discuss some solutions later, but at the beginning, I need to emphasize the fundamental nature of unit testing so that you will not mistakenly think that the rest is about integration testing or acceptance testing.
Once again, the fundamental nature of unit testing is to correctly isolate the code to be tested. If this statement is hard to understand, I will try to give a common example:
If you want to testIs a dog full?So the unit test should not depend onAccording to the time when the dog eats a meal, what is it, how much is it, who feeds it ...... And so on.Can make assertions, especially when these preconditions depend on other components of the system to be generated, you must be careful! Even if this method (for examplea_dog.is_full
) The returned results must depend on the preconditions to be correctly output, and the unit test itself alsoWe should not waste effort on building these preconditions.But should focus onTest and ensure that the returned results of the method are expected and that the returned results of the method will not exceed the expected results under foreseeable Edge Conditions.. As for the various preconditions (including Edge Conditions), you can forge them (which will be described later) instead of calling other code that actually generates them. Only in this way can we ensure "isolation" And can we refer to itUnit Test.
I have seen my colleagues write unit tests, in fact, I can understand their feelings very well, and I also know the crux of the problem (wasting too much energy on creating preconditions for completing assertions). In fact, this layer of window paper is actually worse, as long as you can understand the meaning of the word "isolation" in unit testing, it can be broken.
Timing of TDD
TDD (test-driven development) is a methodology (or practice) that exploits the benefits of testing ). To put it simply, TDD writes tests before writing code, and strictly followsRed => green => refactor(Error => correct => refactoring), so it is called "TestDriver
Development ".Driver
These two words are the core idea of TDD.
However, TDD is not perfect. Many high-level programmers are quite vocal about TDD (non-hierarchical discrimination, but it is easy to understand some problems because of rich experience, most of them are high-level programmers-except me ......), In general, TDD will affect the development efficiency and even impede the smooth development in some extreme situations. Here, I try not to mention the bad words about TDD, but the help it can bring.
The word "Driving" is the core idea of TDD. To avoid the problems that TDD may bring, we must take advantage of the feature that TDD can drive development. So when do developers need to test to drive? I think there are roughly two situations that are appropriate (or even necessary:
Prepare code that you think is "unsung"
I think there may be many reasons for "No spectrum". For example, the business logic is very complicated and I am not completely familiar with it. For example, there is a general idea in my mind, but I have never written it before, it cannot be determined whether unexpected variables will occur during the process.
If there is no test, how do you deal with the above situation? A better idea may be to sort out a list of steps or draw a flowchart or something. If it is worse, you should first hand-write it and then try again. At this time, you should try TDD, but what should you do?
In fact, the basic principle of testing is the same: You give the test case an input, assert the result, and finally execute and observe whether the assertions are correct; and so on, you write N test cases, each of which overwrites some possible input (boundary condition) and asserted the possible results (the results may be returned values or characteristics embodied in some behavior ), then execute the observation result. That's all.
TDD is also a test, so you don't have to think about it as complicated. It follows the same principle, but writes the test without code. Therefore, you do not even need to clarify the entire process of the code. You only need to think about the boundary conditions (this is the input or precondition of the target code. In addition, the significance of the boundary condition is that it is sufficient to overwrite all the conditions without any effort.) The corresponding results can be used to write enough test cases. This is a bit like designing interfaces for large and complex architectures. You don't need to consider the specific implementation but the input and output, which I like to call black box thinking. Then you run the code to check whether it fails, and then write the code to make it successful. Now you have a reliable test case, so you can immediately optimize or refactor the code until it is finally delivered.
This is true for all tests, right? However, TDD itself emphasizes two more things:
From the first test failure to the first test success, this process should not be implemented in one step (unless your code is too simple, but in this case there is no need to be a non-TDD option ). Every time you write the code, the only purpose is to solve the cause of the previous test failure, so that the test produces a new (further) failure until the test is successful, as the saying goes, this is the "small step and fast walk" testing strategy, which has many advantages. For example, you can keep your thinking within the controllable range, each step only needs to solve simple problems and finally solve a complex problem. For example, it helps you write code that is well designed, robust, and easy to understand. Because this process is repeated, the reconstruction process does not have to be put to the end. You can start restructuring somewhere in the middle. It seems that as your ideas become clearer and clearer, you suddenly realize that there are some better ways to use them, so you don't have to worry about it.
Test failures can be divided into two types: one is the exception thrown by the Code (for example, the class or method does not exist, and the name is wrong somewhere ), the other is that the assertion condition is not met (which may also include assertion for exception handling. Pay attention to the difference ). Strictly speaking, only the second type is called test failure. The first type should be the normal response of the language itself (you wrote it wrong). Therefore, you must differentiate the two. In most cases, the output of these two cases is significantly different, so it is not difficult to tell. Never let yourself throw an exception because of the language, but do not know it. Instead, you should make effort and test code hard. Such low-level errors need to be eliminated.
There may be code, improvement, new feature addition, logic for processing new conditions, and Bug fixing. Here I generally classify it as refactoring.
TDD can be said to be born for reconstruction, so it is not surprising that it is well known as Agile Methodology. The biggest difficulty in code refactoring is that you cannot estimate how complicated the old code is (including unexpected and unexpected) in advance ), therefore, every change you make may cause unexpected effects. The word "Butterfly Effect" accurately reflects this problem.
There are differences in the size of refactoring, and TDD cannot be fully integrated for large-scale refactoring, because this is beyond the scope of unit testing capabilities. Large-scale refactoring usually requires top-down, from the outside to the inside. Generally, it is necessary to start from the acceptance test or integration test, and to go deep into the underlying layer and kernel, until the scope is reduced to the level that TDD can cover (for example, to a specific method ).
There are also types of refactoring, sometimes to optimize algorithms, sometimes to solve bugs, and sometimes to add features ...... TDD plays different roles in different types of refactoring. For example, to reconstruct an optimization algorithm, the focus of writing tests is to overwrite the boundary conditions to ensure that the original code logic is not omitted after optimization. In addition, This refactoring usually requires performance tests to know whether the algorithm optimization is effective. This requires the intervention of A/B testing. To solve the Bug reconstruction, the focus of the test is to reproduce the Bug. The cause of a Bug is often complicated and involves the coordination of multiple system components, while unit testing has limited coverage, therefore, we must first use a higher-level testing method to narrow down the scope of bugs that may occur. Adding a function involves code design. Such refactoring will further split and combine the old code to ensure scalability and reusability at a higher abstract level, at this time, the focus of testing is to help you sort out further abstract ideas-this is actually close to writing new code.
To apply TDD for refactoring, the most important thing to note is to break down the old code, which is one of the most common methods for refactoring. When you split a unit (such as a method), make sure that you have enough unit tests to overwrite the original code logic, and then split the complex logic layer by layer, each split (usually one more method) should first have a test case to drive the split code, and in addition to running a new test, run the old test code to ensure that the original code logic is not affected after the split. This process is cumbersome and prone to omissions. You must be good at using the features of automated tools and tip tools (such as eliminating other interference test cases ).
Suggestions for completing tests
Many times, project development is subject to time or human resources that are not qualified to fulfill TDD (or other test-first strategies such as BDD ), when I had the energy to complete the test later, I felt that the dog had nowhere to bite the hedgehog. What should I do at this time? In fact, the best way is to seize every time to solve the Bug.
As mentioned above, the appearance of bugs is very complicated. Simple unit tests often turn into "removing the east wall and making up the west wall ". If your project has reached the completion test stage, it is best to make up from the external, from top to bottom (this is consistent with the premise of large-scale reconstruction). Why?
Because bugs are usually reported by users during usage (I include testers as users), and users are not exposed to a deeper and more internal structure, they all feel the problems of the software system through the user interface. At this time, the best starting point is to reproduce the Bug, and we just said that unit testing cannot cover the user interface level. Therefore, the reproduction of the Bug starts from the acceptance test, after the integration test, the unit test is finally positioned. This not only solves the Bug, but also overwrites a batch of complex system interactions through tests. In this way, you will find that the tests to be completed are almost completed.
Many people call this Bug-driven development. It is actually BDD (behavior-Driven Development, Behaviour Driven Development), but it is only a specific type of behavior, that is, the behavior that causes bugs.
Who will write what kind of tests?
The next topic caused by the completion test is who will write the test. I know that most development teams are either limited in manpower or at a limited level, so testing is basically done by developers themselves. But here I still want to discuss the ideal situation, that is, the ideal environment where there are no bad people or even poor people.
Acceptance Test: Although it is not clear whether there are standards in the industry, the Acceptance Test in my eyes can be divided into two types: one is the Acceptance Test prepared for the end customer, it is usually written by the Project Manager/Product Manager (of course there can be assistant workers), and the other is written by software testers, for internal acceptance by the team-perhaps the Feature Test is more appropriate.
The first category is rare. I have only explained it in the book (I wrote about it myself, but it cannot be used for practice, so it is far beyond the customer's understanding ). This type of test is written in scripting languages that are very similar to natural languages. It looks like a text-type requirement document. People who do not know programming can understand it and can run it, continuous integration and report generation. I think this is a good use of things. In those very large projects, we can effectively replace some hard-earned documents (for Party A's documents ), however, there is a threshold for writers, so it is rare.
Although such tests claim to belong to the BDD category, for most developers, it is a little far away from the code (specifically sensory, so even if we want to practice BDD developers, we would rather choose to write a testing framework more like code, so the development direction of such testing is movingCommercial executable test documentation. If you are interested, refer to Cucumber. It is an acceptance test framework based on the Gherkin language. It supports multi-language and multi-platform platforms and has a paid online version available. (I should have heard of Rails. In the early days, it was just because the BDD name followed by Rails was so popular that it wasn't until RSpec was thriving that it was "just fun ")
The second type (in order to distinguish the first type, we will replace it with the feature test below) is much more useful. As the testing link at the outermost layer of the development work, it is best to write this test by the tester. The feature test reflects the final user-oriented behavior of the software system. It replaces the traditional manual test with an automated script. The basic principle is to work with a browser drive (especially for Web development, i'm not familiar with other fields) to simulate user interaction behavior and test various functional features of the software. There are also Headless Browser drivers that do not need to rely on the GUI for continuous integration, as well as online cross-platform cross-Browser compatibility testing services. These tools/services constitute a group army for feature testing.
Feature testing can also be compiled before code implementation to guide programmers in development, to ensure that they do not have deviations in understanding, and strive for accuracy in implementation. For testers, you don't have to worry about the specific implementation of the code. You only need to run the feature test after the function is released and tested. To some extent, this saves the trouble of testing and development. If the feature test is well written, the product manager/Project Manager does not have to stare at every programmer to explain every detail like a nanny.
Features, or functions, reflect the final behavior of the Code. Feature testing is the final result of code implementation and guidance prior to code implementation. This is the so-called BDD, that is, behavior-driven development. However, the feature test is not the full scope of the BDD function. Next we will discuss another important step --
Integration Test: Integration Test, also called Functional Test, is a Functional Test rather than a Functional Test. Functional testing tests the external performance of a function (feature), mainly for users (including testers; functional testing is to test whether the functionality of one or more components of the system is complete. These components may span multiple functions. For example, the User Authentication component can span multiple functions: Registration, invitation, logon, data modification, and third-party authorization. To avoid confusion, it is better to call integration testing.
The purpose of the integration test is to test the results of the collaborative work of each unit. Therefore, it is the direct "superior" of the unit test. Sometimes it is easy to confuse integration testing and unit testing, because integration testing can be written using the same tool/framework as unit testing. That is to say, integration testing can also practice the practical principles of TDD; of course, the integration test can also be written using a BDD-oriented tool/framework. This boundary is very vague.
Keep in mind: the difference between integrated testing and unit testing is not what tools/frameworks are used, nor what test-driven methods are implemented, who cares more about the isolation of the test code.
Integration testing is concerned about whether several code units can work normally during interaction. Therefore, the code units involved in testing must be real (that is, the actual code you write ), it does not focus on isolation, that is, to check the actual behavior of the Code in the coupling state. unit testing is just the opposite. It only cares about whether a specific code unit works normally, if this kind of work must have external dependencies, unit testing should avoid mixing other real code that does not belong to this unit at the expense of forging These dependencies.
TIPS: Is isolation not considered for integration testing? This is not necessarily true. Sometimes a component in the system depends on external services, such as calling a third-party API. In this case, the request can be properly simulated, which is an isolation, the purpose is to avoid long test cycles caused by remote calls, but ensure the correctness of the simulation.
Theoretically, if unit tests are compiled sufficiently robust, then the combination of these units should work collaboratively through corresponding integration tests. However, in reality, we are not gods, and no one can predict all possible results after the complexity increases. Therefore, integration testing is an important umbrella for system integration behavior and should not be ignored. In fact, if the BDD principle is applied, the integration test should be written before the unit test, that is, "describe its behavior first, and then describe how each unit involved in the behavior implements its own function ".
One more thing is that developers can easily fall into a dilemma, that is, the coverage of feature testing and integration testing is completely equal. In this case, developers can easily write identical feature tests and integration tests. How can this problem be solved? Just remember the phrase "integration testing is internal and feature testing is external.
For example, you want to test user access/test
For integration testing, there may be at least three components: routing, Controller, and view. Therefore, your code should describe the route jump and the method call of the controller, for the process of searching and rendering templates, do not directly touch the final user interface, because this is beyond your responsibility scope.
For feature testing, because it is external, you should examine the process from the perspective of users (including testers. Users do not know what routing controller view ...... He/she only knows: I entered/test
Link, I expect to see the xxx user interface. This is enough. With the help of the browser drive, you can simulate URL jump, get any elements on the page, or simulate more complex user interaction, but these are all happening in the browser (that is, the user interface), and cannot happen in the system (framework.
This is why feature testing should be written by testers because they don't care about what happens outside the user interface. On the other hand, integration testing should be written by programmers themselves, only you know how the system works.
Unit Test: It seems that there is no choice. Can unit testing be the responsibility of programmers? That's right, but aren't we assuming the ideal situation? Therefore, even if unit tests can only be written by programmers, programmers (preferably) should not write unit tests for their own code. Who is here? Other programmers.
That's right. I'm talking about Pair programming. It is certainly not impossible to write unit tests for your own code, but the idea of a person is never open enough-the ancestor said: I don't know the true colors of the mountains, just because I am here. Can you ensure that your ideas are correct? Can you ensure that you have considered all the boundary conditions? Are you sure this is the best way to solve the problem?
No one can do it completely. Even Pair programming is still not perfect, but it is often very good for someone to describe the problem for you. I understand the cost of Pair programming, but many people do not understand the great benefits of Pair programming. writing tests for colleagues (or vice versa) is only one of them. You only know the advantages of this one, I will not talk about it here. In short, if there is any way to quickly improve the quality of unit testing, my answer is Pair programming.
Avoid useless tests
At present, there is no need to write unit tests. The unit tests mentioned here also include tests that do not follow the TDD principles, such as writing code before writing tests.
What is useless testing? I did not say it, but only those who write code really know it. However, if you are new to testing and do not know which ones should be written or not, I have two suggestions:
- Write only necessary tests: What are necessary tests I have already talked about at the time of TDD. If you don't know which tests to write, start from here;
Write only the key test: Sometimes you cannot write the necessary test, and there is no counseling around you, you can barely skip it. However, key tests should not be saved. The so-called key test is the core logic in the code you write. In other words, if everything goes well, it can at least (or never do) Do it. This means that you may ignore the processing of some boundary conditions, and you do not know how to handle them, But you at least ensure that the most important route is accessible. In the future, this critical route will be like the Polaris in the night sky, ensuring that you are not at risk.
If you find it hard to touch the key test cases when constructing them (for example, You won't process the prerequisites in the test cases ), it is possible that your unit is too complicated. This is an excellent signal for Instant reconstruction. You can try to extract the logic to be touched and test it separately. In this way, you have at least separated the core logic, and other code will be much easier to refactor even worse.
As for useless tests, although we cannot list them here, we can also summarize several articles:
Do not test the core library and/or standard library functions of a language: If your code is simple enough to call a standard library function, what time does it take to write a test, these codes have been tested for a long time. Although there are also small probability events with language errors, you cannot touch the processing process of standard functions (often buried in virtual machines or calling underlying system interfaces) therefore, your test is useless to your own code. (Of course, if you are an expert programmer, you are not here. If you are not sure, you are waiting to solve this problem)
Do not test the basic classes or tool methods of the framework. The principle is similar to the first one. Well-known frameworks have their own tests. Otherwise, you will not dare to use them, will they? If you are sure that the framework is faulty, your tests should be applied to the framework itself. Maybe you can make a patch to contribute to the project.
Let's take an example: You inherit the Model layer of a framework, and then define the verification to check whether a certain attribute of its instance is null (use the verification method that comes with the framework, instead of writing it yourself ). In this case, there is no need to test whether this check is effective, unless your class returns an instance of another class during initialization ...... Are there such boring people in your project team?
Do not test the effectiveness of external dependencies: this is a trap that beginners often fall into, and they often torture themselves.
There are two problems: first, if your test must require external dependencies, you should first consider forging it, instead of checking B in A's test.(That is to say, your test target is A. To complete this test case, you need to use B and some characteristics of B must be established. This is A prerequisite, so you have to write an assertion to test this first before you can test the true target). If you can forge a B called B ', then B' doesn't have to be exactly the same as B, as long as it can show exactly the characteristics of this test case. In this way, things will become much simpler.
Second, even if you cannot forge B (basically because it won't), you should at least transfer the test of B's characteristics to B's own unit test.
Finally, there is another test that is "useless", that is, it has never been seen as a red test. You don't realize that this test may not test any code from start to end! This is also one of the reasons why TDD emphasizes "first red, then green" and then refactor. You should at least make the test case fail once at the beginning, otherwise it will be too late to identify it after the number of tests increases. In addition, it is recommended that you manually destroy the code after refactoring (for example, you can enter a few meaningless characters in it) to test and report errors to ensure that the test truly overwrites the target code.
Test code coverage
My attitude towards "useless tests" has revealed my attitude towards code coverage: Ignore it. I have always thought that code coverage is the most formalistic technical tool. A higher coverage rate cannot ensure that the Code itself is impeccable. 100% of the coverage rate in the Bug cannot save you.
In fact, as an auxiliary measurement tool, there is nothing wrong with code coverage. A good friend said: "We should have no choice in the pursuit of excellence ". If the error occurs, the code coverage rate is used as an evaluation indicator to measure the testing staff's working level. I am speechless and quite disgusted with this.
People of insight will certainly say: you should not be partial-width. The code coverage rate measured by path coverage is still quite reliable.
In simple popularization, there are many types of code coverage algorithms. In general, the comparison is accurate: path coverage> conditional coverage ~ = Decision overwrite> statement overwrite. In addition, this is just to say that conditional branches, loops, and other algorithms are not much to say. When the coverage rate of these algorithms reaches 100%, the degree of reliability may be quite different. The problem is that the people who make the decision to use code coverage rate for assessment often do not understand this difference, which gives the people who implement the evaluation the opportunity, it is easy to evolve into "we should have nothing to do with 100% code coverage". Even if the implementation of the people do not understand, it is even more tragic, a group of people to the water of 100% happy mouth can not close, think about it is uncomfortable.
Therefore, improper application of code coverage will only make everyone more and more biased, and the waste of time will not be less effective. In turn, the appropriate use of code coverage has extremely high requirements on the team, it is not enough to know only one person, because you don't have enough time and energy to check whether the results are reliable. If everyone writes code and tests in a reliable way, it is no big deal to test coverage. Therefore, if I am the head of the startup team, I would rather choose to focus my time and energy on the test case itself. the test itself is reliable, and the auxiliary value of the test coverage rate is reliable.
Camouflage in the test"
I have mentioned the "Counterfeit" technology (or "disguise" technology) in the test case many times before. Is it very confusing for beginners? This is an advanced technique for writing and testing. It is not difficult to say it is advanced, but it is abstract in concept. To understand it, you need to think about it. Second, there are many schools of "forged" technology, it is easy to confuse and then daunting, so that you do not dare to use it. I will take the opportunity to talk about my understanding and experiences and hope to help you solve your problems.
Because different platforms in different languages have their own development history and cultural backgrounds, similar or even identical concepts may have different titles, the "camouflage" in the test is a typical example, and its situation is more complex. Many terminologies are related to the "camouflage" in the test. They include:Fakes,Doubles,Stubs,Mocks,Spies...... Maybe there's more I don't know!
By the way, their dynamic nouns, suchMockingRepresents a "disguise" technology, and their Noun Forms, suchMocksRepresents the product after such "Disguise" is applied.
Their complexity is: You can think of them as one thing, but they are different in some details. This is like the old Wang family having five sons. They are both sons of the Old Wang family, but they have their own characteristics. For outsiders (on a higher abstract level ), knowing that they are the sons of the Old Wang family is enough, but for the Old Wang family (more specific), we can tell the differences.
They are also complex: some of them are often equivalent in different language platforms or communities, even some of them will not be mentioned at all (the blurring of such boundaries often comes from the characteristics of the language itself. For example, dynamic languages are more flexible than static languages, usually there is no need to be too true on the object type ).
In view of this, I have no plans to detail them one by one. This is too difficult! I will only introduce their concepts and basic usage, and mention their names when there are clear boundaries.
OK. What is test disguise?
You really like playing chess, but you can't play chess. So what you like most is playing chess with Old Wang. You two players will have a bad temper, playing chess together is the most enjoyable (external dependency needs meet the conditions). If it is another option, you may have to play chess without having fun (external dependency demands do not meet the conditions ).
One day in the middle of the night, you dreamed of being inspired by an old fairy, and your chess force rose three times. This is just a wake-up, and it's just like finding a competitor to get a high level (target to be tested ). But who are you looking? It's hard to get Old Wang out of the bed so late? (The isolation feature of unit testing comes from the uncertainty of external dependencies.) you use a computer to simulate an old man (testing camouflage, it doesn't matter if he is really old Wang. What matters is that "he" can meet your requirements.
As mentioned above, the old king you simulated with a computer is a disguised object-an object created with camouflage. Essentially, it is a common object and can even be created manually. However, for convenience, we often use some specialized camouflage libraries to generate camouflage objects, these camouflage libraries provide us with methods to help us mimic the attributes and behaviors of objects to meet the various needs of test cases. The important thing is that there is no relationship between the disguised object and the real external dependent object, which enables our unit test to take effect on the premise that the external dependency is fully isolated, therefore, even if there is no trace of external dependencies in the actual code, we can still implement the target code on the premise that external dependencies exist.
Camouflage objects are sometimes calledFaked ObjectOrFakes, Also knownDoubled ObjectOrDoublesAnd strict definition of camouflage objects in different language environments, but their principles are consistent. We will useDoubleTo call a disguised object.
Another classification method is to unify all types of camouflage objectsDoubles, And then the subdivision type,Dummies,Fakes,Stubs,Mocks,SpiesThese are sub-types with their respective characteristics.
This article will not be used for academic research. It doesn't matter if you like it.
Camouflage objects can be exactly the same or different from external dependencies. The difference depends on how much your test case depends on. Based on test cases, the characteristics of camouflage objects (which can be understood as simulation degree) are shaped, and various camouflage technologies are developed.
- Type matching of disguised objects
It is not enough for you to simulate an Old Wang. You also hope that this "fake Old Wang" will have the same facial features as the real Old Wang, so that you will have an immersive feeling.
This requires type Matching for external dependencies and needs to simulate their attributes. It is not difficult to simulate attributes, but whether the type matching requirements can be strictly met depends on the language environment and the ability of the test tool. I use RSpec to write a few simple examples (forgive me for not understanding PHP ):
wang = double()# orwang = double('player')
Thiswang
IsDoubleObject.
wang = instance_double('Player')
Thiswang
It must bePlayer
Class, ifPlayer
If the class does not exist, the test throws an error.
wang = instance_double('Player', name: 'Wang')expect(wang.name).to equal('Wang')
This is an example of simulating attributes. It is straightforward (to save space, write only key code ). By the way, ifDoubleThe returned value with a "preset" is used to respond to calls to a specific method. We call itStubThe disguised response technology is calledStubbing. In this examplewang
ThisDoubleIt indicatesname
Attribute, so in the assertionwang.name
A pre-defined response will be returned during the call.Wang
. This example can be more complex (but easier to understand:
wang = instance_double('Player')allow(wang).to receive(:name).and_return('Wang')expect(wang.name).to equal('Wang')
If your English is good, you will find that the previous example is quite smooth! Try to translate once:
wang
YesPlayer
Instance double
Allowwang
Receive messages (method call):name
And returns a string.Wang
Expectedwang.name
Returns a string.Wang
This code style is a typical BDD code style. BDD needs to accurately describe the code behavior, so it will basically create some well-readable DSL (domain-specific language) to help you write a test. However, this is not absolute.
- Behavior matching of disguised objects
In your opinion, it is not enough to look at old Wang, and you need it to play chess like Old Wang.
For example, Old Wang is very fond of using a gun, so once a gun is lost, he will be very angry and play regret games ......
Furthermore, we hope not only to simulate the types and attributes of external dependencies, but also to simulate their behaviors, especially those generated when interacting with the target to be tested. If such a test is not isolated, there will be at least two consequences:
- If the external dependency does not exist, the test will certainly fail (So camouflage is required)
If the external dependency changes, the test will fail. Strictly speaking, such consequences are not the responsibility of testing. Changes to external dependencies should keep the external interfaces unchanged and return results unchanged, and only change internal behaviors. The advantage of camouflage is that once this happens, you will not mistakenly think that your code is correct.
Of course, you will also think that if you use a disguised object, it is not very dangerous to test the external dependency that has changed itself? It makes sense, but unit testing is responsible for testing the correctness of your code. Simulation of external dependencies does not have to be exactly the same as that of the simulated object. Real interaction should be captured by integration testing first, otherwise, it is easy to get lost in complex code interactions.
wang = double(name: 'Wang').as_null_objectcannon = double(side: :black, ...)expect(wang).to receive(:ask_for_takeback).with(cannon).at_least(:once)me.taking(cannon)
You will notice that it is the first thing to assert that Old Wang will require repentance (at least once), and then eat the child. This is like a trigger. First, you declare that the dependent object will certainly show the corresponding behavior after you make a specific behavior. Then, you can try to see if it will happen.
This disguised technique called after assertion is calledMockingIt means "fooling people" (isn't it !), It also has a more academic orthodox title calledMessage expectations. Some programmers do not like this style. They think that it is more common sense to call the assertions first, so it is very similar.Spies, As follows:
me.taking(cannon)expect(wang).to have_received(:ask_for_takeback).with(cannon).at_least(:once)
The above introduction uses RSpec3 as the reference testing framework. For other frameworks in other languages, some concepts and usage may be quite different. This is really difficult for me, so I don't have to trust it. It is important to understand what these technologies are doing and how helpful they are in writing and testing. The specific technical details should be subject to the document of the tool you are using.
Other Test Tools
In my personal daily development, the following tools are also used to write tests:
HTTP Request Interception/simulation
This is mainly used by the front-end. In the test code, there is no need to initiate a request to the backend (mainly asynchronous, that is, Ajax). You only need to intercept real asynchronous requests, then, it is enough to return some pseudo data. Pseudo data can be written to the test code (if the amount is small) and stored as independent files. Fortunately, most of today's data is transmitted using JSON, which is very convenient for Javascript processing.
The recently used tool is prefix, which is simple and easy to use. Of course, the old Sinon is also a good choice, but Sinon is mainly used for mocking/stubbing in principle.
Fixture Generator
Fixture is a special camouflage object, but sometimes it is used simplyDoublesThe simulation is still quite tiring (because the code is repeated), so someone has invented some tools specifically designed to help you generate disguised objects, which can be generated in batch, instantly modified, and randomly generated, reduce your code duplication.
The front-end does not know any good tools (it seems useless). The back-end Ruby mainly uses FactoryGirl.
Ah, I am so tired that I cannot write ...... I also completed the long article summary about the test and achieved another goal! Let's get there first. Wait until you think about it and add it again, or if you have any questions, add the answer.
- Timing of unit testing: Software Development
Don't count on completing unit testing unless you do have nothing to do.
- The role of writing unit tests: developers, writing unit tests, in fact, also give themselves a chance to optimize and refactor code, because in order to increase testability, you need to consider decoupling
- Advantages of unit test writing: easy to discover problems (whether it is a change in demand or a bug in the Code itself) and Code refactoring
To address your issue, programmers generally write test cases before development.