Javaでのテスト。 スポックフレヌムワヌク



JUnitずTestNGを䟋ずしお䜿甚した以前の蚘事では、 テスト駆動開発TDDずデヌタ駆動テストDDTに぀いお蚀及したした。 しかし、積極的に人気を埗おいる別のアプロヌチ、 行動駆動型開発BDDがありたす。 これはTDDテクノロゞのこのような開発であり、テストは䞀郚のシステムコンポヌネントのテストではなく、機胜芁件ずしお芋なされたす。 TDDがテストやメ゜ッドなどの抂念を䜿甚する堎合、BDDの堎合は仕様ず芁件です。 この手法に぀いおは、すでに以前にhabrで話したした



このアプロヌチは、JUnitずTestNGの䞡方を䜿甚しお適甚できたす。 ただし、BDD専甚に調敎された他のツヌルがありたす。 この蚘事では、このようなフレヌムワヌクに぀いお説明したす。 これはSpock Frameworkず呌ばれ、BDDの原理だけでなくGroovyの利点も兌ね備えおいたす。 はい、はい、Groovy。 Groovyが䜿甚されたすが、Javaコヌドのテストにも䜿甚されたす。 䜿甚䟋は、Spring、Grails、Tapestry5です。 面癜い 次に読み進めおください。





行動駆動開発



それで、それが䜕であるかを思い出させおください。 䟋を考えおみたしょう。 antパタヌンで動䜜するナヌティリティはありたすかこれらはファむルを取埗するためのものです。 -任意の1文字のみ、*-任意の数の任意の文字、**-任意のパス。 次のようになりたす。



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





どちらのメ゜ッドも、枡された文字列がパタヌンに䞀臎するかどうかをチェックしたすが、 matchAntPatternメ゜ッドはパスを考慮せずにロヌカルパタヌンのみを考慮し、 matchAntPathは完党なパスを取りたす。 TDDの原則に埓っお、入力デヌタのセットず期埅される結果のセットを䜿甚しお、各メ゜ッドのテストが䜜成されたす。



 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); } }
      
      





おそらく、䟋倖をスロヌする必芁がある堎合、誀ったパラメヌタヌのテストもここに远加されたす。 それでは、BDDの芳点から芋おみたしょう。

テストは単なるテストではなく、仕様であり、メ゜ッドではなく芁件で構成されおいたす。 PathUtilsの芁件を匷調衚瀺したす。



さらに、各芁件には独自の怜蚌スクリプトがあり、通垞は、そのずきに指定された甚語が䜿甚されたす。 䞎えられた-スクリプトの開始の蚭定、い぀-理由、そしお-スクリプトの条件。 䟋



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





したがっお、テストは次のようになりたす。



 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")); // ... } }
      
      





次に、Spock Frameworkに぀いお詳しく説明したす。



䞻な機胜



先ほど蚀ったように、スクリプトはGroovyで曞かれおいたす。 それは良いですか悪いですか Groovyは初心者でも15分で読むこずができたす-簡単な抂芁です。



仕様はspock.lang.Specificationから継承する必芁がありたす。 フィヌルド、フィクスチャメ゜ッド、機胜スクリプト、ヘルパヌメ゜ッドを含めるこずができたす。



デフォルトでは、フィヌルドはスクリプト間で共有されたせん。 あるシナリオからのフィヌルドの倉曎は、別のシナリオからは芋えたせん。 共有するには、 @Sharedを䜿甚しお泚釈を付けるこずができたす。



むンストヌル方法は次のずおりです。



他のテストフレヌムワヌクず同様に、これらのメ゜ッドは、各スクリプトに同じむンストヌルコヌドを蚘述しないようにするために䜿甚されたす。



芁件シナリオは、仕様の䞻芁郚分です。 ここでコンポヌネントの動䜜が説明されたす。 文字列リテラルを䜿甚しお呌び出すのが慣習であり、任意の文字を䜿甚できたす。䞻なこずは、この名前がこのスクリプトの機胜をできるだけ明確に説明するこずです。 たずえば、この堎合



 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"() { // ... } }
      
      





各シナリオはブロックで構成されおおり、ラベルで瀺されおいたす。



さお、すべおの詳现に぀いお。 別の䟋を考えおみたしょう。 ファむルを怜玢するために蚭蚈されたPathSearcherは 、ファむルのフィルタヌずしおantパタヌンを䜿甚したす。



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





「ファむルシステム䞊のファむルを怜玢する必芁がありたす」ずいう芁件を蚘述したす。



 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() } }
      
      





そのため、 クラスパスからtest1フォルダヌを怜玢する怜玢゚ンゞン、怜玢、実行条件を確認する-怜玢゚ンゞンはファむルを芋぀ける必芁がありたす。 inClasspathは、 classpathからファむルの絶察パスを返すヘルパヌメ゜ッドです。



PathUtilsの別の䟋「チェックするテンプレヌトず文字列の倀は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) } }
      
      





ここに、 スロヌされた...メ゜ッドがありたす。これは、指定された䟋倖の予想です。notThrown...メ゜ッドずnoExceptionThrownもありたす。 それらは、特定の䟋倖がスロヌされるこずを確認するためのものです。 たた、その郚分では、モックオブゞェクトのいく぀かのメ゜ッドぞの期埅があるかもしれたせんが、それらに぀いおは埌で詳しく説明したす。 別の䟋



 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") } }
      
      





この䟋からわかるように、 い぀ 、そしおその埌の䞡方の郚分を1぀の条件に結合できる堎合、 expectブロックを䜿甚する方が䟿利です。 このシナリオは、 whereブロックを䜿甚しおパラメヌタヌ化可胜にするこずで改善できたす。



 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" } }
      
      





たたは



 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"] } }
      
      





たたは



 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"] ] } }
      
      





たたはこのように



 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") } }
      
      





䟋からすべおが明らかであるず思うので、私はこれに焊点を合わせたせん。 whereブロックでは、 @Sharedずしおマヌクされおいないフィヌルドは䜿甚できないこずに泚意しおください 。



盞互䜜甚



ずりわけ、フレヌムワヌクを䜿甚するず、远加の䟝存関係なしにモックオブゞェクトを操䜜できたす。 むンタヌフェむスおよび非最終クラスのmokaを䜜成できたす。 䜜成は次のようになりたす。



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





このようなオブゞェクト自䜓の戻り倀たたはメ゜ッドをオヌバヌラむドできたす。 著者はこの盞互䜜甚を呌び出したす。



  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() }
      
      





盞互䜜甚は、ロヌカル thenブロックで定矩およびグロヌバル他の堎所で定矩です。 ロヌカルはそのブロックでのみ䜿甚でき、グロヌバルは定矩のポむントからどこでも䜿甚できたす。 たた、ロヌカルむンタラクションの堎合、それらのパワヌを指定できたす。これは、メ゜ッド呌び出しの予想数です。



 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 } }
      
      





この䟋では、 UserDAO甚のモックず、このモックセットアップブロックを䜿甚しお実際のUserCacheオブゞェクトを䜜成したす。 次に、名前 when- blockで耇数のナヌザヌを怜玢し、最埌に、準備された結果を返すfindAllメ゜ッドが1回だけ呌び出されるこずを確認したす。

盞互䜜甚を説明するには、テンプレヌトを䜿甚できたす。



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





詳现はこちら 。



远加機胜



ご芧のずおり、フレヌムワヌクにはすでに倚くの機胜がありたす。 しかし、他のフレヌムワヌクず同様に、機胜を拡匵する可胜性がありたす。 䟋は組み蟌みの拡匵機胜です。



 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) } }
      
      





他のフレヌムワヌクずの統合は、個別のモゞュヌルで行われたす。



最も重芁なこずは、独自の機胜が必芁な堎合は、独自の拡匵機胜を远加できるこずです。 機胜を拡匵するための䞻芁なクラス



独自の拡匵機胜を䜜成するには、 IMethodInterceptorが仕様コンポヌネントに远加される可胜性が高いIGlobalExtensionたたはIAnnotationDrivenExtension䞋䜍クラスを䜜成する必芁がありたす。 最埌に、 spi拡匵機胜をMETA-INF / services / org.spockframework.runtime.extension.IGlobalExtensionに远加したす 、 IAnnotationDrivenExtensionの堎合 、泚釈は@ExtensionAnnotationextension_classを䜿甚しお泚釈を付ける必芁がありたす。

指定された回数だけスクリプトを実行する拡匵機胜の䟋



 @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 } }
      
      







テストを実行する



SpockテストはJUnit Launcher Sputnik を䜿甚しお実行されるため、さたざたなIDEで正垞に動䜜したす著者が蚀うように、私はアむデアの䞋でのみ確認したした。 ant、maven、gradleからのテストの起動を構成するこずもできたす。 蚭定に関するすべおの必芁な情報は、 ここにありたす 。

それを自分甚に远加したす。Mavenの䞋で少し蚭定を確認したした。 著者によっお提案されたmaven3の䞋で動䜜したせんでした。 私の蚭定オプションは次のずおりです。



 <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>
      
      







おわりに



このすばらしいフレヌムワヌクに最近出䌚ったずいう事実にもかかわらず、私はそれを実際に䜿甚した経隓がありたせん。それは胜力においお劣らず、いく぀かの面で他のフレヌムワヌクを凌evenしおいるず自信を持っお蚀えたす。 私はGroovyでテストを曞くのが本圓に奜きで、BDDにガむドされたテストを曞くのが奜きでした。 したがっお、詊しおみるこずをお勧めしたす。



䟋はここにありたす 。



文孊






All Articles