Preface
Recently, the team introduced Spock testing framework. After I used it in practice, I had a very good experience. At the same time, I learned the relevant knowledge of unit testing in the whole learning process.
The purpose of this article is to consolidate the input knowledge and to promote it to all of you.
Before we learn about the Spock testing framework, we should first focus on the unit test itself and understand our common single point of pain so that we can better understand what the Spock testing framework is, why we use it, and what kind of pain we can solve.
Let's start now.
About Unit Testing
We have to test when we write code. There are many kinds of tests. For Javaer, the initial test is to write a main function to run a function result, or to start the system to simulate the request itself, to see if the input and output are in line with expectations, and more advanced, to test the system with various test suites. Each test has its own concerns, such as whether the test function is correct, system performance bottlenecks and so on.
What about the unit tests we often talk about?
Unit Testing, also known as modular testing, aims at Program module)(software design Minimum unit) to carry out correctness testing. Program unit is the smallest testable component of application. stay Procedural Programming A unit is a single program, function, process, etc. For object-oriented programming, the smallest unit is the method, including the method in the base class (superclass), Abstract class, or derived class (subclass).
- from Wikipedia
The above is the explanation of the word.
Unit testing is certainly not a must. Without testing your program through end-to-end testing and integration testing by the QA team, it can also ensure correctness. But from another point of view, unit testing is also a must. For example, one of the prerequisites for continuous deployment is the protection of unit testing, and when there is no unit testing, you won't be able to move on.
Benefits of 1.1 Unit Testing
The benefits of unit testing include, but are not limited to:
- Improving software quality
High quality unit testing can guarantee the quality of development and the robustness of the program. The earlier the defect is found, the lower the cost of repair is.
- Promoting code optimization
The developer and maintainer of unit test are all development engineers. In this process, developers will constantly examine their code, so as to optimize their own code.
- Improving R&D efficiency
Writing unit tests apparently takes up the time of project development, but in the following stages of debugging, integration and regression testing, the code with high coverage of single tests has fewer defects and problems have been repaired, which is helpful to improve the overall R&D efficiency.
- Increasing Reconstructive Self-Confidence
Code refactoring usually involves lower-level changes, such as modifying the underlying data structure, etc. Upper-level services are often affected; under the protection of unit testing, we will have more confidence in the reconstructed code.
Basic Principles of 1.2 Unit Testing
Macroscopically, unit testing should conform to AIR principles:
- A: Automation
- I: Independent
- R: Repeatable
Microscopically, the unit test code level should conform to BCDE principles:
- B: Border, boundary testing, including loop boundaries, special values, special time points, data sequence, etc.
- C: Correct, correct input, and expected results**
- D: Design, in line with design documents, to write unit tests
- E: Error, the purpose of unit testing is to prove the program is wrong, not to prove the program is right. In order to find hidden errors in the code, we need to have some mandatory error inputs (such as illegal data, exception processes, non-business allowed input, etc.) when writing test cases to get the expected error results.
Common scenarios for 1.3 unit testing
- Unit tests are written before development. Requirements are described through tests, and development is driven by tests. (If you are not familiar with TDD, you can go to google.)
- In the process of development, get feedback in time and find problems in advance.
- Applied to automated build or continuous integration processes, do regression tests for each code modification. (CI/CD Quality Assurance)
- As the basis of refactoring, verify the reliability of refactoring.
Common Pain Points in Unit 1.4 Testing
The following pain points were encountered in my daily development.
- Test context depends on external services, such as database services
- There are code dependencies in the test context (such as frameworks, etc.)
- Unit testing is difficult to maintain and understand (ambiguous semantics)
- For functions with different inputs and outputs in multiple scenarios, unit test code can be a lot larger.
- ...
I'll explain a little bit about the above points.
First of all, the amount of code tested is absolutely no less than that of business code (assuming coverage metrics and no cheating). Sometimes a function has multiple input and output situations. If you want full coverage, the amount of code will only be more. A lot of code quantity, plus single test code does not want to be as intuitive as business code (by writing annotation, watching chaos, writing tired), and a number of coding personnel do not attach importance to code readability, eventually leading to unit test code difficult to read, and more difficult to maintain. At the same time, most of the unit testing frameworks are very intrusive to the code. To understand unit testing, you must first learn about that unit testing framework. From this point of view, maintenance becomes more difficult.
In addition, unit testing has external dependencies, that is, the first and second points. It is often difficult to write a pure independent unit test, such as relying on database and other modules. So many people choose to rely on some resources when writing unit tests, such as starting a database locally. Such so-called "unit tests" are often popular, but for multi-person collaborative projects, such tests are often confusing. For example, to read a file locally, or to connect to a database, other people who modify the code (or continuous integration systems) don't have these things, so the test can't pass either. In the end, most of these test codes are useless and can't be deleted, so they have to be commented out and thrown there. With the gradual development of open source projects, the dependence on external resources can be solved by some test aids, such as using memory database H2 instead of connecting to the actual test database, but the types of resources that can be replaced are always limited.
In the actual work process, there is another kind of dependency problem which is difficult to deal with: code dependency. For example, the method of one object calls the method of other objects, and other objects call more objects, and finally form a huge call tree. Later, some mock frameworks emerged, such as JMockit, EasyMock, or Mockito in java. Using such a framework, it is relatively easy to make assumptions and verifications through mock mode, which has a qualitative leap over the previous way.
However, it is important to emphasize here that the difficulty of writing unit tests is most related to the quality of the code and is decisive. No matter which testing framework is used in the project, it can not solve the problem that the code itself is difficult to test.
Simply put, sometimes you find it difficult to write unit tests, which means that the code is not well written. You need to pay attention to whether the logical abstraction design of the code is reasonable, and reconstruct your code step by step to make your code easier to test. But these belong to the knowledge of code refactoring, involving many design principles. It is recommended to read "Refactoring - Improving the Design of Existing Codes", "The Art of Modifying Codes", "Agile Software Development: Principles, Patterns and Practice".
1.5 Mental Change
Many developers have psychological barriers to unit testing.
- That's what testing classmates do. (Developers need to do unit testing well
- Unit test code is redundant. (The overall function of the car is strongly related to the normal testing of each unit.
- Unit test code does not require maintenance. After a year and a half, it's almost abandoned (unit test code needs to be maintained along with project development)
- Unit testing has no dialectical relationship with on-line failures. (Good unit testing can minimize on-line failures
About Spock
Spock takes a step backwards to provide you with all the testing tools you might need throughout the test life cycle. It comes with built-in analog piling and some additional test annotations specifically created for integrated testing. At the same time, because Spock is a relatively new testing framework, it has time to observe the common defects of existing frameworks and to solve them or provide more elegant solutions. Explaining all the advantages of Spock over existing solutions is beyond the scope of this article.
- Spock is a testing and specification framework for Java and Groovy applications
- Test code uses specification language, which is an extension of groovy-based language.
- Through junit runner call test, compatible with most junit run scenarios (ide, build tools, continuous integration, etc.)
Groovy
- JVM Language Designed for "Extending JAVA"
- JAVA Developer Friendly
- You can use java grammar and API
- Simplified grammar with strong expressiveness
- Typical applications: jenkins, elastic search, gradle
specification language
Specification comes from the recently popular BDD (Behavior-driven development behavior-driven testing). Based on TDD, code behavior is expressed through testing. A specification language is used to describe what the program should do, then read these descriptions through a test framework and verify that the application is in line with expectations. Convert requirements into Given/When/Then three-paragraph, so you can see that the test framework has this Given/When/Then three-paragraph grammar, which is generally backed by BDD ideas, such as Cucumber and JBehave in the figure above.
Spock Quick Use
3.0 Create a blank project
Create a blank project: spock-example, select the maven project.
3.1 dependence
<dependencies> <!-- Mandatory dependencies for using Spock --> <!-- Use Spock Necessary dependence --> <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-core</artifactId> <version>1.3-groovy-2.5</version> <scope>test</scope> </dependency> <!-- Optional dependencies for using Spock --> <!-- Selective Spock Dependent dependence --> <dependency> <!-- use a specific Groovy version rather than the one specified by spock-core --> <!-- No use Spock-core Defined in Groovy Version, but by definition --> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.5.7</version> <type>pom</type> </dependency> <dependency> <!-- enables mocking of classes (in addition to interfaces) --> <!-- mock Interfaces and classes need to be used --> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.9.3</version> <scope>test</scope> </dependency> <dependency> <!-- enables mocking of classes without default constructor (together with CGLIB) --> <!-- mock Class to use --> <groupId>org.objenesis</groupId> <artifactId>objenesis</artifactId> <version>2.6</version> <scope>test</scope> </dependency> <dependency> <!-- only required if Hamcrest matchers are used --> <!-- Hamcrest Is a framework for writing matching objects, if used Hamcrest matchers,Need to add this dependency --> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> <version>1.3</version> <scope>test</scope> </dependency> <!-- Dependencies used by examples in this project (not required for using Spock) --> <!-- Use h2base Do a test database--> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.197</version> <scope>test</scope> </dependency> </dependencies>
3.2 plug-in
<plugins> <!-- Mandatory plugins for using Spock --> <!--Use Spock Mandatory plug-ins --> <plugin> <!-- The gmavenplus plugin is used to compile Groovy code. To learn more about this plugin,visit https://github.com/groovy/GMavenPlus/wiki --> <!-- this gmavenplus Plug-ins are used for compilation Groovy Code . Want to get more information about this plug-in,visit https://github.com/groovy/GMavenPlus/wiki --> <groupId>org.codehaus.gmavenplus</groupId> <artifactId>gmavenplus-plugin</artifactId> <version>1.6</version> <executions> <execution> <goals> <goal>compile</goal> <goal>compileTests</goal> </goals> </execution> </executions> </plugin> <!-- Optional plugins for using Spock --> <!-- Selective Spock Related plug-ins--> <!-- Only required if names of spec classes don't match default Surefire patterns (`*Test` etc.) --> <!--Only if the test class does not match the default Surefire patterns (`*Test` Wait.)--> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.20.1</version> <configuration> <useFile>false</useFile> <includes> <include>**/*Test.java</include> <include>**/*Spec.java</include> </includes> </configuration> </plugin> ... </plugins>
3.3 Design and Test Source Directory
Because spock is groovy-based, you need to create groovy's test source directory: first create a directory named groovy under the test directory, and then set it as a test source directory.
3.4 Write the class to be tested
/** * @author Richard_yyf * @version 1.0 2019/10/1 */ public class Calculator { public int size(String str){ return str.length(); } public int sum(int a, int b) { return a + b; } }
3.5 Create Test Classes
Ctrl + Shift + T
import spock.lang.Specification import spock.lang.Subject import spock.lang.Title import spock.lang.Unroll /** * * @author Richard_yyf * @version 1.0 2019/10/1 */ @Title("Test Calculator Class") @Subject(Calculator) class CalculatorSpec extends Specification { def calculator = new Calculator() void setup() { } void cleanup() { } def "should return the real size of the input string"() { expect: str.size() == length where: str | length "Spock" | 5 "Kirk" | 4 "Scotty" | 6 } // The test failed. def "should return a+b value"() { expect: calculator.sum(1,1) == 1 } // No Chinese is recommended. @Unroll def "The return value is the sum of the input values."() { expect: c == calculator.sum(a, b) where: a | b | c 1 | 2 | 3 2 | 3 | 5 10 | 2 | 12 } }
3.6 Running Test
3.7 Analog Dependence
Here we simulate a caching service as an example
/** * @author Richard_yyf * @version 1.0 2019/10/2 */ public interface CacheService { String getUserName(); }
public class Calculator { private CacheService cacheService; public Calculator(CacheService cacheService) { this.cacheService = cacheService; } public boolean isLoggedInUser(String userName) { return Objects.equals(userName, cacheService.getUserName()); } ... }
Test class
class CalculatorSpec extends Specification { // mock object // CacheService cacheService = Mock() def cacheService = Mock(CacheService) def calculator void setup() { calculator = new Calculator(cacheService) } def "is username equal to logged in username"() { // stub piling cacheService.getUserName(*_) >> "Richard" when: def result = calculator.isLoggedInUser("Richard") then: result } ... }
Operation test
Spock depth
In pock, the behavior of the system under test (SUT) is defined by specifications. When writing tests using the Spock framework, the test class needs to inherit from the Specification class. Naming follows the Java specification.
Spock infrastructure
Each test method can use text as its method name directly, and the method is composed of given-when-then block. In addition, there are several different blocks such as and, where, expect and so on.
@Title("Title of the test") @Narrative("""Large Text Description of Testing""") @Subject(Adder) //Indicate that the class being tested is Adder @Stepwise //When there is a dependency between test methods, labeling test methods will be executed strictly in the order they are declared in the source code class TestCaseClass extends Specification { @Shared //Data shared between test methods SomeClass sharedObj def setupSpec() { //TODO: Setting up the environment for each test class } def setup() { //TODO: Set up the environment for each test method, and execute each test method once } @Ignore("Ignore this test method") @Issue(["problem#23","problem#34"]) def "Test Method 1" () { given: "Given a precondition" //TODO: code here and: "Other preconditions" expect: "Assertions that are available everywhere" //TODO: code here when: "When a particular event occurs" //TODO: code here and: "Other trigger conditions" then: "Postresult" //TODO: code here and: "Other concurrent results" where: "Not required test data" input1 | input2 || output ... | ... || ... } @IgnoreRest //Only test this method, ignoring all other methods @Timeout(value = 50, unit = TimeUnit.MILLISECONDS) // Set the timeout time of the test method in seconds by default def "Test Method 2"() { //TODO: code here } def cleanup() { //TODO: Clean up the environment for each test method and execute each test method once } def cleanupSepc() { //TODO: Clean up the environment for each test class }
Feature methods
It is the core of Spock Specification, which describes the various behaviors that SUT should have. Each Specification contains a set of related Feature methods:
def "should return a+b value"() { expect: calculator.sum(1,1) == 1 }
blocks
Each feature method is divided into different blocks. Different blocks are in different stages of test execution. When the test runs, each block is executed in different order and rules, as follows:
-
Setup Blocks
setup can also be written as given, where initializers associated with the test function are placed, such as:
def "is username equal to logged in username"() { setup: def str = "Richard" // stub piling cacheService.getUserName(*_) >> str when: def result = calculator.isLoggedInUser("Richard") then: result }
- When and Then Blocks
When and then need to be used together, perform functions to be tested in when, and judge whether they meet expectations in then.
-
Expect Blocks
expect can be seen as a condensed version of when+then, such as
when: def x = Math.max(1, 2) then: x == 2
reduce to
expect: Math.max(1, 2) == 2
Assertion
Conditions are similar to assert in junit. As in the example above, assert defaults to boolean-type top-level statements with all return values in the then or expect. If you want to add assertions elsewhere, you need to explicitly add the assert keyword
Abnormal assertion
If you want to verify whether an exception has been thrown, thrown()
def "peek"() { when: stack.peek() then: thrown(EmptyStackException) }
If you want to verify that no exception is thrown, you can use notThrown()
Mock
Mock is the act of describing the interaction between an object under a specification and its collaborators.
1 * subscriber.receive("hello") | | | | | | | argument constraint | | method constraint | target constraint cardinality
Creating Mock Objects
def subscriber = Mock(Subscriber) def subscriber2 = Mock(Subscriber) Subscriber subscriber = Mock() Subscriber subscriber2 = Mock()
Injecting Mock objects
class PublisherSpec extends Specification { Publisher publisher = new Publisher() Subscriber subscriber = Mock() Subscriber subscriber2 = Mock() def setup() { publisher.subscribers << subscriber // << is a Groovy shorthand for List.add() publisher.subscribers << subscriber2 }
Call frequency constraint
1 * subscriber.receive("hello") // exactly one call 0 * subscriber.receive("hello") // zero calls (1..3) * subscriber.receive("hello") // between one and three calls (inclusive) (1.._) * subscriber.receive("hello") // at least one call (_..3) * subscriber.receive("hello") // at most three calls _ * subscriber.receive("hello") // any number of calls, including zero // (rarely needed; see 'Strict Mocking')
Target constraint
1 * subscriber.receive("hello") // a call to 'subscriber' 1 * _.receive("hello") // a call to any mock object
Method constraint
1 * subscriber.receive("hello") // a method named 'receive' 1 * subscriber./r.*e/("hello") // a method whose name matches the given regular expression (here: method name starts with 'r' and ends in 'e')
Argument constraint
1 * subscriber.receive("hello") // an argument that is equal to the String "hello" 1 * subscriber.receive(!"hello") // an argument that is unequal to the String "hello" 1 * subscriber.receive() // the empty argument list (would never match in our example) 1 * subscriber.receive(_) // any single argument (including null) 1 * subscriber.receive(*_) // any argument list (including the empty argument list) 1 * subscriber.receive(!null) // any non-null argument 1 * subscriber.receive(_ as String) // any non-null argument that is-a String 1 * subscriber.receive(endsWith("lo")) // any non-null argument that is-a String 1 * subscriber.receive({ it.size() > 3 && it.contains('a') }) // an argument that satisfies the given predicate, meaning that // code argument constraints need to return true of false // depending on whether they match or not // (here: message length is greater than 3 and contains the character a)
Stub piling
Stubbing is the act of allowing collaborators to respond in some way to method calls. When a method is stubbed, it does not care about the number of calls to the method, but hopes that it will return some values or perform some side effects when it is called.
subscriber.receive(_) >> "ok" | | | | | | | response generator | | argument constraint | method constraint target constraint
For example, subscriber.receive (")" ok "means that no matter what instances, parameters, and calls receive methods return to string ok
Returns a fixed value
Use the > operator to return a fixed value
subscriber.receive(_) >> "ok"
Return value sequence
Returns a sequence, iterates and returns the specified value in turn. As shown below, the first call returns ok, the second call returns error, and so on.
subscriber.receive(_) >>> ["ok", "error", "error", "ok"]
Dynamic calculation of return value
subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" } subscriber.receive(_) >> { String message -> message.size() > 3 ? "ok" : "fail" }
Side effects
subscriber.receive(_) >> { throw new InternalError("ouch") }
Chain response
subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"
epilogue
This paper introduces the basic knowledge of unit testing and some usage of Spokek. With Spock, you can enjoy the groovy scripting language's convenient, one-stop test suite, and the test code written is more elegant and readable.
But it's only the first step to learn how to use a testing framework. It's just the first step to learn how to use Spock. How to make good use of Spock requires a lot of soft changes, such as how to write a test case, how to progressively reconstruct the code and write more testable code, how to make the team implement TDD, and so on.
I hope to share more relevant knowledge in the future.