Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

4Developers 2015: Testowanie ze Spockiem - Dominik Przybysz

193 views

Published on

Czy można pisać testy bez takich bibliotek jak JUnit, TestNG i festAssert? Mockować bez Mockito? Łapać wyjątki bez catch-exception? Oczywiście, że można. Odpowiedzią jest Spock.
Podczas prezentacji przyjrzymy się możliwościom Spocka w testowaniu kodu javowego i grooviowego. Zobaczymy jak pisać w stylu given-when-then, sprawdząć warunki nie używając słowa "assert", jak zasilać testy danymi, udawać, że mamy zależności testowanego obiektu i pisać testy integracyjne z podniesionym kontekstem springa.

Published in: Software
  • Be the first to comment

  • Be the first to like this

4Developers 2015: Testowanie ze Spockiem - Dominik Przybysz

  1. 1. Spock Dominik Przybysz https://github.com/alien11689/spock-show alien11689@gmail.com @alien11689 http://przybyszd.blogspot.com
  2. 2. Context
  3. 3. Person.groovy @Canonical class Person { String firstName String lastName Integer age boolean isAdult() { age >= 18 } }
  4. 4. PersonValidator.java Part. 1 @Component public class PersonValidator { public void validatePerson(Person person) { String firstName = person.getFirstName(); if (firstName == null || firstName.length() == 0) { throw new PersonValidationException("First name must be given"); } String lastName = person.getLastName(); if (lastName == null || lastName.length() == 0) { throw new PersonValidationException("Last name must be given"); }
  5. 5. PersonValidator.java Part. 2 Integer age = person.getAge(); if (age == null){ throw new PersonValidationException("Age must be given"); } if( age < 0) { throw new PersonValidationException("Age cannot be negative"); } } }
  6. 6. PersonValidationException.java public class PersonValidationException extends RuntimeException { public PersonValidationException(String message) { super(message); } }
  7. 7. PersonDao.groovy Part. 1 @Component class PersonDao { final JdbcTemplate jdbcTemplate @Autowired PersonDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate }
  8. 8. PersonDao.groovy Part. 2 void persist(List<Person> persons) { persons.each { persist(it) } } @Transactional void persist(Person person) { jdbcTemplate.execute("Insert into person (first_name, last_name, age) values ('${person. firstName}', '${person.lastName}', ${person.age})") }
  9. 9. PersonDao.groovy Part. 3 List<Person> findByLastName(String lastName) { jdbcTemplate.queryForList("select first_name, last_name, age from person where last_name = ?", [lastName] as Object[]) .collect({Map row -> new Person(row.first_name, row.last_name, row.age) }) } void close() { println "Closing person dao" } }
  10. 10. PersonController.groovy Part. 1 @Component class PersonController { final PersonValidator personValidator final PersonDao personDao @Autowired PersonController(PersonValidator personValidator, PersonDao personDao) { this.personValidator = personValidator this.personDao = personDao }
  11. 11. PersonController.groovy Part. 2 void addPerson(Person person) { personValidator.validatePerson(person) personDao.persist(person) } }
  12. 12. PersonContextConfiguration.groovy Part. 1 @Configuration @ComponentScan("com.blogspot.przybyszd.spock") class PersonContextConfiguration { @Bean JdbcTemplate getJdbcTemplate(DataSource dataSource){ return new JdbcTemplate(dataSource) }
  13. 13. PersonContextConfiguration.groovy Part. 2 @Bean DataSource getDataSource() { BasicDataSource basicDataSource = new BasicDataSource() basicDataSource .setDriverClassName("org.h2.Driver") basicDataSource .setUrl("jdbc:h2:mem:personDB;DB_CLOSE_DELAY=1000; INIT=runscript from 'classpath:db/person.sql';") basicDataSource.setUsername("sa") basicDataSource.setPassword("") return basicDataSource } }
  14. 14. Introduction
  15. 15. Dependencies compile 'org.codehaus.groovy:groovy-all:2.4.0' compile 'org.springframework:spring-jdbc:4.0.5.RELEASE' compile 'org.springframework:spring-beans:4.0.5.RELEASE' compile 'org.springframework:spring-context:4.0.5.RELEASE' compile 'commons-dbcp:commons-dbcp:1.4' compile 'com.h2database:h2:1.4.178' testCompile 'org.spockframework:spock-core:1.0-groovy-2.4' testCompile 'org.spockframework:spock-spring:1.0-groovy- 2.4' testCompile 'org.springframework:spring-test:4.0.5. RELEASE' testCompile 'cglib:cglib-nodep:3.1' testCompile 'org.objenesis:objenesis:2.1'
  16. 16. when-then blocks class PersonTest extends Specification { def "should set first name from constructor"() { when: Person person = new Person(firstName: "Bob") then: person.firstName == "Bob" } }
  17. 17. Test failed output person.firstName == "Bob" | | | | Bb false | 1 difference (66% similarity) | B(-)b | B(o)b com.blogspot.przybyszd.spock.dto.Person(Bb, null, null)
  18. 18. Block with description def "should set first name from constructor 2"() { when: "person with set first name" Person person = new Person(firstName: "Bob") then: "person has first name" person.firstName == "Bob" }
  19. 19. Given block def "should set first name from setter"() { given: Person person = new Person(firstName: "Bob") when: person.firstName = 'Tom' then: person.firstName == "Tom" }
  20. 20. Multiple asserts def "should set person data from constructor"() { when: Person person = new Person("Bob", "Smith", 15) then: person.firstName == "Bob" person.lastName == "Smith" person.age == 15 }
  21. 21. Multiple when then def "should set first name from constructor and change with setter"() { when: Person person = new Person(firstName: "Bob") then: person.firstName == "Bob" when: person.firstName = "Tom" then: person.firstName == "Tom" }
  22. 22. And block def "should set first name and last name"() { when: Person person = new Person(firstName: "Bob", lastName: "Smith") then: person.firstName == "Bob" and: person.lastName == "Smith" }
  23. 23. Expect block def "should compare person with equals"() { expect: new Person("Bob", "Smith", 15) == new Person ("Bob", "Smith", 15) }
  24. 24. Lifecycle
  25. 25. Test fields class LifecycleSpockTest extends Specification { @Shared StringWriter writer Person person }
  26. 26. Setup specification def setupSpec() { println "In setup spec" writer = new StringWriter() }
  27. 27. Setup each test def setup() { println "In setup" person = new Person(firstName: "Tom", lastName: "Smith", age: 21) }
  28. 28. Cleanup each test def cleanup() { println "In cleanup" person = null }
  29. 29. Cleanup specification def cleanupSpec() { println "In cleanup spec" writer.close() }
  30. 30. Setup and clenup blocks def "should check firstName"() { setup: println "setup in test" println "should check firstName" expect: person.firstName == "Tom" cleanup: println "Cleanup after test" }
  31. 31. Statements without block def "should check lastName"() { println "should check lastName" expect: person.lastName == "Smith" }
  32. 32. Parameters
  33. 33. Parameters in table @Unroll def "should set person data"() { when: Person person = new Person(lastName: lastName, firstName: firstName, age: age) then: person.firstName == firstName person.lastName == lastName person.age == age where: lastName | firstName | age "Smith" | "John" | 25 "Kowalski" | "Jan" | 24 }
  34. 34. Parameters in method signature @Unroll def "should set person data 2"(String firstName, String lastName, int age) { // … where: lastName | firstName | age "Smith" | "John" | 25 "Kowalski" | "Jan" | 24 }
  35. 35. Parameters in method name @Unroll def "should set person with #lastName, #firstName and #age"() { // … where: lastName | firstName | age "Smith" | "John" | 25 "Kowalski" | "Jan" | 24 }
  36. 36. Call parameterless method in test name Part 1 @Unroll("should set person with #lastName.length(), #firstName.toUpperCase() and #age")
  37. 37. Call parameterless method in test name Part 2 @Unroll("should set person with #lastName.length(), #firstName.toUpperCase() and #age when last name starts with #firstLetter") def "should set person with lastName, firstName and age 3"() { //… where: lastName | firstName | age "Smith" | "John" | 25 "Kowalski" | "Jan" | 24 firstLetter = lastName.charAt(0) }
  38. 38. Separeted table @Unroll def "should check if person is adult with table"() { expect: new Person(age: age).isAdult() == adult where: age || adult 17 || false 18 || true 19 || true }
  39. 39. Parameters from list @Unroll def "should check if person is adult with list"() { expect: new Person(age: age).isAdult() == adult ageSquare == age * age where: age << [17, 18, 19] adult << [false, true, true] ageSquare = age * age }
  40. 40. Parameters from list of list @Unroll def "should check if person is adult with list 2"() { expect: new Person(age: age).isAdult() == adult where: [age, adult] << [[17,false], [18,true], [19, true]] }
  41. 41. One paramter table @Unroll def "should set first name"() { when: Person person = new Person(firstName: firstName) then: person.firstName == firstName where: firstName | _ "John" | _ "Jan" | _ }
  42. 42. Parameters from db - setup static Sql sql = Sql.newInstance("jdbc:h2:mem:", "sa", "", "org.h2.Driver") def setupSpec() { sql.execute("""DROP TABLE IF EXISTS person; CREATE TABLE person ( first_name VARCHAR(256) NOT NULL, last_name VARCHAR(256) NOT NULL, age INT NOT NULL );""") sql.executeInsert("""INSERT INTO person (first_name, last_name, age) VALUES ('Tom', 'Smith', 24), ('Jan', 'Kowalski', 30);""") }
  43. 43. Parameters from db - cleanup def cleanupSpec() { sql.close() }
  44. 44. All parameters from db @Unroll def "should set person data with #lastName, #firstName and #age"() { // … where: [firstName, lastName, age] << sql.rows("SELECT * FROM person;") }
  45. 45. All parameters from db by name @Unroll def "should set person data with #lastName, #firstName and #age"() { // … where: [firstName, lastName, age] << sql.rows("SELECT first_name, last_name, age FROM person;") }
  46. 46. Drop last parameter @Unroll def "should set person data with #lastName, #firstName and #age"() { // … where: [firstName, lastName] << sql.rows("SELECT * FROM person;") }
  47. 47. Omit parameter @Unroll def "should set person data with #lastName, #firstName and #age"() { // … where: [_, lastName, age] << sql.rows("SELECT * FROM person;") }
  48. 48. Exceptions
  49. 49. Not thrown exception PersonValidator sut = new PersonValidator() def "should pass validation"() { given: Person person = new Person(firstName: "Tom", lastName: "Smith", age: 30) when: sut.validatePerson(person) then: notThrown(PersonValidationException) }
  50. 50. Not thrown exception - fails Expected no exception of type 'com.blogspot.przybyszd. spock.bean.PersonValidationException' to be thrown, but got it nevertheless
  51. 51. Thrown exception @Unroll def "should not pass validation"() { then: PersonValidationException exception = thrown (PersonValidationException) exception.message == message where: firstName | lastName | age | message "Tom" | "Smith" | -1 | "Age cannot be negative" "" | "Kowalski" | 19 | "First name must be given" "Jan" | null | 19 | "Last name must be given" }
  52. 52. Thrown exception - another exception Expected exception of type 'com.blogspot.przybyszd. spock.bean.PersonValidationException', but got 'java. lang.RuntimeException'
  53. 53. Thrown exception - but no exception Expected exception of type 'com.blogspot.przybyszd. spock.bean.PersonValidationException', but no exception was thrown
  54. 54. Mocking and stubbing
  55. 55. Creating mock JdbcTemplate jdbcTemplate = Mock(JdbcTemplate) PersonDao sut = new PersonDao(jdbcTemplate)
  56. 56. Validate mock calls def "should persist one person"() { given: Person person = new Person("John", "Smith", 20) when: sut.persist(person) then: 1 * jdbcTemplate.execute("Insert into person (first_name, last_name, age) values ('John', 'Smith', 20)") }
  57. 57. Mock not called Too few invocations for: 1 * jdbcTemplate.execute("Insert into person (first_name, last_name, age) values ('John', 'Smith', 20)") (0 invocations) Unmatched invocations (ordered by similarity): None
  58. 58. Too many calls 1 * jdbcTemplate.execute("Insert into person (first_name, last_name, age) values ('John', 'Smith', 20)") (2 invocations) Matching invocations (ordered by last occurrence): 2 * jdbcTemplate.execute('Insert into person (first_name, last_name, age) values ('John', 'Smith', 20)') <-- this triggered the error
  59. 59. Another parameters in calls def "should persist many persons"() { given: List<Person> persons = [new Person("John", "Smith", 20), new Person("Jan", "Kowalski", 15)] when: sut.persist(persons) then: 1 * jdbcTemplate.execute("Insert into person (first_name, last_name, age) values ('John', 'Smith', 20)") 1 * jdbcTemplate.execute("Insert into person (first_name, last_name, age) values ('Jan', 'Kowalski', 15)") }
  60. 60. Any parameter then: 2 * jdbcTemplate.execute(_)
  61. 61. Range of calls then: (1..3) * jdbcTemplate.execute(_)
  62. 62. At least one call then: (1.._) * jdbcTemplate.execute(_)
  63. 63. Any amount of calls then: _ * jdbcTemplate.execute(_)
  64. 64. Two calls of method of any mock then: 2 * _.execute(_)
  65. 65. Two calls of any method of mock then: 2 * jdbcTemplate._(_)
  66. 66. Two calls of method by regex then: 2 * jdbcTemplate./exe.*/(_)
  67. 67. Closure validates call then: 2 * jdbcTemplate.execute({ String sql -> sql.endsWith("('John', 'Smith', 20)") || sql.endsWith("('Jan', 'Kowalski', 15)") })
  68. 68. Sequential calls def "should persist many persons in order"() { given: List<Person> persons = [new Person("John", "Smith", 20), new Person("Jan", "Kowalski", 15)] when: sut.persist(persons) then: 1 * jdbcTemplate.execute("Insert into person (first_name, last_name, age) values ('John', 'Smith', 20)") then: 1 * jdbcTemplate.execute("Insert into person (first_name, last_name, age) values ('Jan', 'Kowalski', 15)") }
  69. 69. Define mock interactions in given block given: jdbcTemplate = Mock(JdbcTemplate) { 2 * execute({ String sql -> sql.endsWith("('John', 'Smith', 20)") || sql.endsWith("('Jan', 'Kowalski', 15)") }) }
  70. 70. Stub def "should find one person"() { given: jdbcTemplate.queryForList("select first_name, last_name, age from person where last_name = ?", ["Kowalski"]) >> [[first_name: "Jan", last_name: "Kowalski", age: 20]] expect: sut.findByLastName("Kowalski") == [new Person ("Jan", "Kowalski", 20)] }
  71. 71. Stub in context given: jdbcTemplate = Stub(JdbcTemplate) { queryForList("select first_name, last_name, age from person where last_name = ?", ["Kowalski"]) >> [[first_name: "Jan", last_name: "Kowalski", age: 20]] }
  72. 72. Any stub parameters def "should find many times person"() { given: jdbcTemplate.queryForList(_, _) >> [[first_name: "Jan", last_name: "Kowalski", age: 20]] expect: sut.findByLastName("Kowalski") == [new Person ("Jan", "Kowalski", 20)] sut.findByLastName("Kowalski") == [new Person ("Jan", "Kowalski", 20)] }
  73. 73. Multiple return values def "should find many times person 2"() { given: jdbcTemplate.queryForList(_, _) >> [[first_name: "Jan", last_name: "Kowalski", age: 20]] >> [[first_name: "Jan", last_name: "Kowalski", age: 25]] expect: sut.findByLastName("Kowalski") == [new Person ("Jan", "Kowalski", 20)] sut.findByLastName("Kowalski") == [new Person ("Jan", "Kowalski", 25)] }
  74. 74. Multiple return values as list def "should find many times person 3"() { given: jdbcTemplate.queryForList(_, _) >>> [ [[first_name: "Jan", last_name: "Kowalski", age: 20]], [[first_name: "Jan", last_name: "Kowalski", age: 15]]] expect: sut.findByLastName("Kowalski") == [new Person ("Jan", "Kowalski", 20)] sut.findByLastName("Kowalski") == [new Person ("Jan", "Kowalski", 15)] }
  75. 75. Side effects def "should throw exception on second find"() { given: jdbcTemplate.queryForList(_, _) >> [[first_name: "Jan", last_name: "Kowalski", age: 20]] >> { throw new DataRetrievalFailureException("Cannot retrieve data") } expect: sut.findByLastName("Kowalski") == [new Person ("Jan", "Kowalski", 20)] when: sut.findByLastName("Kowalski") then: thrown(DataAccessException) }
  76. 76. Mocking and stubbing def "should find one person and check invocation"() { when: List result = sut.findByLastName("Kowalski") then: result == [new Person("Jan", "Kowalski", 20)] 1 * jdbcTemplate.queryForList(_, _) >> [[first_name: "Jan", last_name: "Kowalski", age: 20]] }
  77. 77. Any parameter list then: 1 * jdbcTemplate.queryForList(*_) >> [[first_name: "Jan", last_name: "Kowalski", age: 20]]
  78. 78. Validate parameter value then: 1 * jdbcTemplate.queryForList(_, !(["Smith"] as Object[])) >> [[first_name: "Jan", last_name: "Kowalski", age: 20]]
  79. 79. Validate parameter is not null then: 1 * jdbcTemplate.queryForList(!null, _) >> [[first_name: "Jan", last_name: "Kowalski", age: 20]]
  80. 80. Interaction block def "should find one person and check invocation external with first parameter not null"() { when: List result = sut.findByLastName("Kowalski") then: result == [new Person("Jan", "Kowalski", 20)] interaction { queryForListCalledOnceWithFirstName() } } void queryForListCalledOnceWithFirstName(){ 1 * jdbcTemplate.queryForList(!null, _) >> [[first_name: "Jan", last_name: "Kowalski", age: 20]] }
  81. 81. Spies List sut = Spy(ArrayList, constructorArgs: [10]) def "should use spy on list"() { given: sut.add(1) >> { callRealMethod() } sut.size() >> 10 when: sut.add(1) then: sut.size() == 10 sut.get(0) == 1 }
  82. 82. Spring
  83. 83. Spring from configuration class @ContextConfiguration(classes = PersonContextConfiguration) class PersonContextFromClassTest extends Specification { @Autowired PersonController personController @Autowired PersonDao personDao //… }
  84. 84. Spring from configuration xml @ContextConfiguration(locations = "classpath: personContext.xml") class PersonContextFromXmlTest extends Specification { @Autowired PersonController personController @Autowired PersonDao personDao //… }
  85. 85. Helper methods
  86. 86. Without helper def "should check person"() { when: Person result = new Person("Tom", "Smith", 20) then: result != null result.firstName == "Tom" result.lastName == "Smith" result.age == 20 }
  87. 87. Boolean helper def "should check person with boolean helper method"() { when: Person result = new Person("Tom", "Smith", 20) then: checkPerson(result, "Tom", "Smith", 20) } boolean checkPerson(Person person, String firstName, String lastName, int age) { person != null && person.firstName == firstName && person.lastName == lastName && person.age == age }
  88. 88. Boolean helper - output checkPerson(result, "Tom", "Smith", 20) | | false com.blogspot.przybyszd.spock.dto.Person (Tom, Smith, 20)
  89. 89. Helper with assert def "should check person with assert helper method"() { when: Person result = new Person("Tom", "Smith", 20) then: checkPersonWithAssert(result, "Tom", "Smith", 20) } void checkPersonWithAssert(Person person, String firstName, String lastName, int age) { assert person != null assert person.firstName == firstName assert person.lastName == lastName assert person.age == age }
  90. 90. Helper with assert - output person.firstName == "John" | | | | Tom false | 3 differences (25% similarity) | (T)o(m-) | (J)o(hn) com.blogspot.przybyszd.spock.dto.Person(Tom, Smith, 20)
  91. 91. With def "should set first name, last name and age 1"() { when: Person person = new Person(firstName: "Bob", lastName: "Smith", age: 40) then: with(person) { firstName == "Bob" lastName == "Smith" age == 40 } }
  92. 92. Annotations
  93. 93. Ignore class IgnoreTest extends Specification { def "test 1"() { expect: 1 == 1 } @Ignore def "test 2"() { expect: 1 == 1 } def "test 3"() { expect: 1 == 1 } }
  94. 94. IgnoreRest class IgnoreRestTest extends Specification { def "test 1"() { expect: 1 == 1 } @IgnoreRest def "test 2"() { expect: 1 == 1 } def "test 3"() { expect: 1 == 1 } }
  95. 95. IgnoreIf class IgnoreIfTest extends Specification { // @IgnoreIf({os.windows}) // @IgnoreIf({os.linux}) @IgnoreIf({ System.getProperty("os.name").contains ("Linux") }) def "test 1"() { expect: 1 == 1 } }
  96. 96. Requires class RequiresTest extends Specification { // @Requires({os.windows}) // @Requires({os.linux}) @Requires({ System.getProperty("os.name").contains ("windows") }) def "test 1"() { expect: 1 == 1 } }
  97. 97. AutoCleanup class AutoCleanupTest extends Specification { JdbcTemplate jdbcTemplate = Mock(JdbcTemplate) @AutoCleanup(value = "close", quiet = true) PersonDao sut = new PersonDao(jdbcTemplate) def "test 1"() { expect: sut != null } }
  98. 98. FailsWith class FailsWithTest extends Specification { @FailsWith(RuntimeException) def "test 1"() { expect: throw new RuntimeException() } }
  99. 99. Timeout class TimeoutTest extends Specification { @Timeout(value = 750, unit = TimeUnit.MILLISECONDS) def "test 1"() { expect: 1 == 1 } }
  100. 100. Title @Title("Title annotation is tested in this specification") class TitleTest extends Specification { def "test 1"() { expect: 1 == 1 } }
  101. 101. Narrative @Narrative("""Multiline narrative annotation is tested in this specification""") class NarrativeTest extends Specification { def "test 1"() { expect: 1 == 1 } }
  102. 102. Subject @Subject(Person) class SubjectTest extends Specification { @Subject Person person = new Person("John", "Smith", 21) def "should be adult"() { expect: person.isAdult() } }
  103. 103. Issue class IssueTest extends Specification { @Issue(["http://example.org/mantis/view.php? id=12345", "http://example.org/mantis/view.php?id=23"]) def "test 1"() { expect: 1 == 1 } }
  104. 104. Extra
  105. 105. void instead of def void "should set first name from constructor"() { when: Person person = new Person(firstName: "Bob") then: person.firstName == "Bob" }
  106. 106. Shoud instead of def Should "set first name from constructor"() { when: Person person = new Person(firstName: "Bob") then: person.firstName == "Bob" }
  107. 107. Shoud instead of def - how it works import java.lang.Void as Should
  108. 108. Q & A
  109. 109. Thank you

×