⬆️ ⬇️

Testing in Java. Spock framework



In previous articles, using JUnit and TestNG as examples, I mentioned test-driven development (TDD) and data-driven testing (DDT) . But there is another actively gaining popularity approach, behavior-driven development (BDD) . This is a development of TDD technology, in which the test is not looked at as testing some components of the system, but as the requirements for functionality. If TDD operates with such concepts as a test or a method, then for BDD it is the specification and requirements. About this technique already talked about Habré earlier:



This approach is applicable using both JUnit and TestNG. But there are other tools sharpened just under BDD. In this article I will talk about such a framework. It is called Spock Framework and combines not only the principles of BDD, but also the merits of Groovy . Yes, yes, exactly Groovy. Although Groovy is used, it is also used to test Java code. Examples of use are Spring, Grails, Tapestry5. Interesting? Then read on.





Behavior-driven development



So let me remind you what it is. Consider an example. There is a utility that works with ant templates (these are the ones for selecting files).? - any 1 and only 1 character, * - any number of any characters, ** - any way. It looks something like this:



public abstract class PathUtils { public static boolean matchAntPath(final String path, final String pattern) { // ... } public static boolean matchAntPattern(final String path, final String pattern) { // ... } } 


Both methods check whether the passed string matches the pattern or not, but the matchAntPattern method only considers the local pattern without the path, matchAntPath takes the full path into account. Following the principles of TDD, a test will be created for each method with some set of input data and some set of expected results.

')

 public class TestPathUtils extends Assert { @Test(dataProvider = "matchAntPatternData") public void testMatchAntPattern(final String pattern, final String text, final boolean expected) { final boolean actual = PathUtils.matchAntPattern(text, pattern); assertEquals(actual, expected); } @Test(dataProvider = "matchAntPathData") public void testMatchAntPath(final String pattern, final String path, final boolean expected) { final boolean actual = PathUtils.matchAntPath(path, pattern); assertEquals(actual, expected); } } 


Perhaps there will also be added tests for incorrect parameters when exceptions should be thrown. Now let's look at it from the point of view of BDD.

A test is not just a test, but a specification, and consists not of methods, but of requirements. Select the requirements for PathUtils :



Further, each requirement has its own verification script, usually using the terms given-once (given-when-then). Given - settings for the beginning of the script, when - the reason, then - the condition of the script. For example:



 Given: PathUtils --- When: matchAntPattern(null, "some string") --- Then: NullPointerException should be thrown 


So the test will look something like this:



 public class PathUtilsSpec extends Assert { @Test public void question_character_should_mean_any_character() { assertTrue(PathUtils.matchAntPattern("abb", "a?b")); assertTrue(PathUtils.matchAntPattern("a1b", "a?b")); assertTrue(PathUtils.matchAntPattern("a@b", "a?b")); assertTrue(PathUtils.matchAntPath("abb", "a?b")); assertTrue(PathUtils.matchAntPath("a1b", "a?b")); assertTrue(PathUtils.matchAntPath("a@b", "a?b")); // ... } @Test public void question_character_should_mean_only_one_character() { assertFalse(PathUtils.matchAntPattern("ab", "a?b")); assertFalse(PathUtils.matchAntPattern("aabb", "a?b")); assertFalse(PathUtils.matchAntPath("ab", "a?b")); assertFalse(PathUtils.matchAntPath("aabb", "a?b")); // ... } @Test public void asterisk_character_should_mean_any_character() { assertTrue(PathUtils.matchAntPattern("abb", "a*b")); assertTrue(PathUtils.matchAntPattern("a1b", "a*b")); assertTrue(PathUtils.matchAntPattern("a@b", "a*b")); assertTrue(PathUtils.matchAntPath("abb", "a*b")); assertTrue(PathUtils.matchAntPath("a1b", "a*b")); assertTrue(PathUtils.matchAntPath("a@b", "a*b")); // ... } @Test public void asterisk_character_should_mean_any_number_of_characters() { assertTrue(PathUtils.matchAntPattern("ab", "a*b")); assertTrue(PathUtils.matchAntPattern("aabb", "a*b")); assertTrue(PathUtils.matchAntPath("ab", "a*b")); assertTrue(PathUtils.matchAntPath("aabb", "a*b")); // ... } @Test public void double_asterisk_character_should_mean_any_path() { assertTrue(PathUtils.matchAntPath("aaa/bbb", "aaa/**/bbb")); assertTrue(PathUtils.matchAntPath("aaa/ccc/bbb", "aaa/**/bbb")); assertTrue(PathUtils.matchAntPath("aaa/c/c/c/bbb", "aaa/**/bbb")); // ... } } 


Now more about the Spock Framework.



Main features



As I said before, scripts are written in Groovy. Is it good or bad? Solve it yourself, beginners can read Groovy in 15 minutes - a quick overview .



The specification must be inherited from the spock.lang.Specification . It may contain fields, installation methods (fixture methods), requirement scripts (feature methods), and helper methods.



The default fields are not scrambled between scripts, i.e. changes to a field from one script will not be visible from another script. To share you can zannotirovat using @Shared .



Installation methods are:



As in other test frameworks, these methods are used to not write the same installation code for each script.



Requirement scenarios are the main part of the specification. This is where the behavior of the component is described. It is customary to call them using string literals, and you can use any characters, the main thing is that this name describes as clearly as possible what this script does. For example, in our case:



 class PathUtilsSpec extends Specification { def "? character should mean any character"() { // ... } def "? character should mean only one character"() { // ... } def "* character should mean any character"() { // ... } def "* character should mean any number of characters"() { // ... } def "** character should mean any path"() { // ... } } 


Each script consists of blocks, which are labeled:



Now about everything in more detail. Consider another example. PathSearcher , designed to find files, uses ant-templates as a filter for files.



 public class PathSearcher { public PathSearcher(final String path) {...} public PathSearcher include(final String... patterns) {...} public PathSearcher exclude(final String... patterns) {...} public Set<String> search() {...} } 


Let us write to it the requirement “must search for files on the file system”:



 class PathSearcherSpec extends Specification { def "it should search files under the file system"() { given: def searcher = PathSearcher.create(inClasspath("test1")) when: def results = searcher.search(); then: results.containsAll(["1.txt", "2.txt"]); results.size() == 2 } private String inClasspath(path) { return ClassLoader.getSystemResource(path).toExternalForm() } } 


So, given - the search engine that searches the test1 folder from the classpath , we check the search, the execution condition - the search engine must find our files. inClasspath is a helper method that returns the absolute path of the file from the classpath .



Another example for PathUtils "The pattern values ​​and the string being checked should not be null."



 class PathUtilsSpec extends Specification { def "null parameter values are not allowed"() { when: PathUtils.matchAntPattern(null, "some string") then: thrown(NullPointerException) when: PathUtils.matchAntPattern("some string", null) then: thrown(NullPointerException) when: PathUtils.matchAntPath(null, "some string") then: thrown(NullPointerException) when: PathUtils.matchAntPath("some string", null) then: thrown(NullPointerException) } } 


Here we see the thrown (...) method, this is the expectation of the specified exception, there is also the notThrown (...) method and noExceptionThrown () . They are to verify that the specified / no exception is thrown. Also in then parts there can be expectations of execution of some methods for mock objects, but about them a bit later. One more example:



 class PathUtilsSpec extends Specification { def "? character should mean any character"() { expect: PathUtils.matchAntPattern("abb", "a?b") PathUtils.matchAntPattern("a1b", "a?b") PathUtils.matchAntPattern("a@b", "a?b") PathUtils.matchAntPath("abb", "a?b") PathUtils.matchAntPath("a1b", "a?b") PathUtils.matchAntPath("a@b", "a?b") } } 


As you can see from the example, if both when and then parts can be combined into one condition, it is more convenient to use the expect block. This scenario can be improved by making it parameterizable using the where block:



 class PathUtilsSpec extends Specification { def "? character should mean any character"() { expect: PathUtils.matchAntPattern(text, pattern) PathUtils.matchAntPath(text, pattern) where: pattern | text "ab?" | "abc" "ab?" | "ab1" "ab?" | "ab@" "a?b" | "abb" "a?b" | "a1b" "a?b" | "a@b" "?ab" | "aab" "?ab" | "1ab" "?ab" | "@ab" } } 


Or so:



 class PathUtilsSpec extends Specification { def "? character should mean any character"() { expect: PathUtils.matchAntPattern(text, pattern) PathUtils.matchAntPath(text, pattern) where: pattern << ["ab?", "ab?", "ab?", "a?b", "a?b", "a?b", "?ab", "?ab", "?ab"] text << ["abc", "ab1", "ab@", "abb", "a1b", "a@b", "aab", "1ab", "@ab"] } } 


Or so:



 class PathUtilsSpec extends Specification { def "? character should mean any character"() { expect: PathUtils.matchAntPattern(text, pattern) PathUtils.matchAntPath(text, pattern) where: [pattern, text] << [ ["ab?", "abc"], ["ab?", "ab1"], ["ab?", "ab@"], ["a?b", "abb"], ["a?b", "a1b"], ["a?b", "a@b"], ["?ab", "aab"], ["?ab", "1ab"], ["?ab", "@ab"] ] } } 


Or even like this:



 class PathUtilsSpec extends Specification { def "? character should mean any character"() { expect: PathUtils.matchAntPattern(text, pattern) PathUtils.matchAntPath(text, pattern) where: [pattern, text] = sql.execute("select pattern, text from path_utils_test") } } 


I think everything is clear from the examples, so I will not focus on this. I will just note that in the where block you cannot use fields that are not marked as @Shared .



Interactions



In addition, the framework allows you to work with the mock objects without additional dependencies. Moki can be created for interfaces and non-final classes. The creation looks like this:



  def dao1 = Mock(UserDAO) UserDAO dao2 = Mock() 


You can override the return values ​​or the methods of such objects themselves. Authors call this interactions.



  dao1.findAll() >> [ new User(name: "test1", description: "Test User"), new User(name: "test2", description: "Test User"), new User(name: "test3", description: "Test User") ] dao2.findAll() >> { throw new UnsupportedOperationException() } 


Interactions are local (defined in the then block) and global (defined elsewhere). Local ones are available only in the then block, global ones are available everywhere, starting from the point of their definition. Also for local interactions, you can specify their power, this is the expected number of method calls.



 class UserCacheSpec extends Specification { def users = [ new User(name: "test1", description: "Test User"), new User(name: "test2", description: "Test User"), new User(name: "test3", description: "Test User") ] def "dao should be used only once for all user searches until invalidated"() { setup: def dao = Mock(UserDAO) def cache = new UserCacheImpl(dao) when: cache.getUser("test1") cache.getUser("test2") cache.getUser("test3") cache.getUser("test4") then: 1 * dao.findAll() >> users } } 


In this example, we create a mock for UserDAO and a real UserCache object using this mock (setup block). Then we search for several users by name ( when -block) and finally check that the findAll method, which returns a previously prepared result, is called only once.

Describing the interaction, you can use templates:



  1 * dao.findAll() >> users (1..4) * dao.findAll() >> users (2.._) * dao.findAll() >> users (_..4) * dao.findAll() >> users _.findAll() >> users dao./find.*/(_) >> users 


You can read more here .



Additional features



As you can see, the framework already has a lot of features. But like other frameworks there is the possibility of expanding the functionality. Examples include embedded extensions:



 class InternalExtensionsSpec extends Specification { @FailsWith(NumberFormatException) @Unroll("#featureName (#data)") def "integer parse method should throw exception for wrong parameters"() { Integer.parseInt(data) where: data << ["Hello, World!!!", "0x245", "1798237199878129387197238"] } @Ignore @Timeout(3) def "temporary disabled feature"() { setup: sleep(20000) } } 


Integration with other frameworks made in separate modules:



And most importantly, if you need your own functionality, you can add your own extensions. Key classes for extending the functionality:



To create your own extension, you need to create a descendant class IGlobalExtension or IAnnotationDrivenExtension , in which your IMethodInterceptor will most likely be added to the components of the specification, and in the end for IGlobalExtension you need to add the spi extension to META-INF / services / orgpopoframework.runtime.extension.IGlobalExtension , for IAnnotationDrivenExtension, our annotation needs to be annotated using @ExtensionAnnotation (extension_class) .

An example of an extension that runs a script a specified number of times:



 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) @ExtensionAnnotation(RepeatExtension.class) public @interface Repeat { int value() default 1; } public class RepeatExtension extends AbstractAnnotationDrivenExtension<Repeat> { @Override public void visitFeatureAnnotation(Repeat annotation, FeatureInfo feature) { feature.addInterceptor(new RepeatInterceptor(annotation.value())); } } public class RepeatInterceptor extends AbstractMethodInterceptor{ private final int count; public RepeatInterceptor(int count) { this.count = count; } @Override public void interceptFeatureExecution(IMethodInvocation invocation) throws Throwable { for (int i = 0; i < count; i++) { invocation.proceed(); } } } 




 class CustomExtensionsSpec extends Specification { @Repeat(10) def "custom extension"() { expect: Integer.parseInt("123") == 123 } } 




Running tests



Due to the fact that Spock tests are run using JUnit launchers ( Sputnik ), they work fine under various IDEs (as the authors say, I checked only under the idea). You can also configure the test run from ant, maven, gradle. All the necessary information about the settings can be found here .

I will also add that for myself I’m a little podshamanil configuration under maven, because proposed by the authors did not work under maven3. Here is my configuration option:



 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.example</groupId> <artifactId>testing-example</artifactId> <version>1.0-SNAPSHOT</version> </parent> <groupId>com.example</groupId> <artifactId>testing-spock</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>Testing Spock Framework Example</name> <description> This is an example application that demonstrates Spock Framework usage. </description> <dependencies> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>${groovy-version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-core</artifactId> <version>${spock.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <testResources> <testResource> <directory>src/test/groovy</directory> </testResource> <testResource> <directory>src/test/resources</directory> </testResource> </testResources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <includes> <include>**/*Spec.groovy</include> </includes> </configuration> </plugin> <plugin> <groupId>org.codehaus.gmaven</groupId> <artifactId>gmaven-plugin</artifactId> <version>${gmaven-version}</version> <configuration> <providerSelection>${gmaven-provider}</providerSelection> </configuration> <executions> <execution> <goals> <goal>testCompile</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>${groovy-version}</version> </dependency> </dependencies> </plugin> </plugins> </build> <properties> <groovy-version>1.7.10</groovy-version> <gmaven-version>1.3</gmaven-version> <gmaven-provider>1.7</gmaven-provider> <spock.version>0.5-groovy-1.7</spock.version> </properties> </project> 




Conclusion



Despite the fact that I got acquainted with this wonderful framework quite recently and practically have no experience of using it, I can say with confidence that it is not inferior in its capabilities, and in some moments even surpasses other frameworks. I really liked to write tests on Groovy, I liked to write tests guided by BDD. Therefore, I advise you to try.



Examples can be found here .



Literature



Source: https://habr.com/ru/post/137561/



All Articles