Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Developer Tests - Things to Know (Vilnius JUG)

Developer Tests - Things to Know (Vilnius JUG)

There are many great talks that discuss challenges developers face when writing software tests. In this talk let's look at test design problems that may seem to be simple but yet fundamentally important and often misunderstood even by experienced programmers.

Vaidas Pilkauskas

December 11, 2014
Tweet

More Decks by Vaidas Pilkauskas

Other Decks in Programming

Transcript

  1. me My hobbies • developer at Wix.com • main language

    - Scala • main professional interest - developer communities If there is time left after my hobbies • mountain bicycle rider, snowboarder • consumer of rock music, contemporary art, etc
  2. me - how to contact me connect with me on

    LinkedIn http://lt.linkedin. com/pub/vaidas-pilkauskas/8/77/863/ add me on G+ https://www.google.com/+VaidasPilkauskas follow on Twitter @liucijus
  3. “We couldn’t understand why people without technical knowledge had to

    tell programmers “what” to do and, furthermore, they had to supervise “how” programmers did it.” Cristian Rennella http://qz.com/260846/why-our-startup-has-no-bosses-no-office-and-a-four-day-work-week/
  4. What this talk is about • Things we argue about

    during code reviews • Things that took me time to understand and prove that they are actually good way to go • Small things we have no time to discuss during big talks
  5. So what is test? It is system’s exercise under predefined

    conditions and then verification of an expected outcome.
  6. Test phases in code Server server = new NotesServer(); //

    setup Note note = new Note("test note"); // setup Status status = server.add(note); // exercise assertEquals(SUCCESS, status); // verify server.shutdown(); // teardown
  7. Start everything in one method @Test public void serverShouldAddNoteSuccessfully() {

    Server server = new NotesServer(); // setup Note note = new Note("test note"); // setup Status status = server.add(note); // exercise assertEquals(SUCCESS, status); // verify server.shutdown(); // teardown }
  8. Refactor to lifecycle methods @Before public void before() { server

    = new NotesServer(); // setup note = new Note("test note"); // setup } @Test public void serverShouldAddNoteSuccessfully() { Status status = server.add(note); // exercise assertEquals(SUCCESS, status); // verify } @After public void after() { server.shutdown(); // teardown }
  9. DON’Ts - #2 Do not mix setup code from different

    tests - shared code must be relevant to the tests that use it
  10. DON’Ts - #3 Setup and teardown are there to solve

    DRY problem and to help structure your test code
  11. Typical setup inside @Before @Before public void before() { server

    = new NotesServer(); // setup note = new Note("test note"); // setup } @Test public void serverShouldAddNoteSuccessfully() { Status status = server.add(note); // exercise assertEquals(SUCCESS, status); // verify } @After public void after() { server.shutdown(); // teardown }
  12. Setup pollution - #2 • It is tempting to add

    additional setup tuning just to fix/enhance one test.
  13. Setting up “job” inside test method @Before public void before()

    { server = new NotesServer(); // setup } @Test public void serverShouldAddNoteSuccessfully() { note = new Note("test note"); // setup Status status = server.add(note); // exercise assertEquals(SUCCESS, status); // verify }
  14. But then be more specific @Test public void serverShouldAddSingleLineNoteSuccesfully() {

    // * set up which is actual for the current method // * use scope specific name Note singleLineNote = new Note("test note"); // setup Status status = server.add(singleLineNote); // exercise assertEquals(SUCCESS, status); // verify }
  15. Summary of test code organization • DRY principle. • Readability.

    BDD vs. DRY • Consistency. Maintain the same style across your codebase. • Complexity. It may dictate the way you go.
  16. Refactoring Refactoring is about improving the design of existing code.

    It is the process of changing a software system in such a way that it does not alter the external behavior of the code, yet improves its internal structure. Martin Fowler Refactoring: Improving the Design of Existing Code
  17. Test behaviour not methods • Think of a contract •

    And responsibilities • Specify requirements as tests
  18. Test behaviour not methods • Think of a contract •

    And responsibilities • Specify requirements as tests • Happens naturally when done in test-first approach
  19. Matchers • Enhance readability • Asserts on the right level

    of abstraction • Encapsulate testing logic • Reusable • Detailed match error messages (do not leave them out in your custom matchers!)
  20. Matchers • Enhance readability • Asserts on the right level

    of abstraction • Encapsulate testing logic • Reusable • Detailed match error messages (do not leave them out in your custom matchers!)
  21. Matcher libraries • Hamcrest - standard matcher lib for JUnit

    • AssertJ - fluent assertions (IDE friendly) • Provides common matchers • You can write your own custom matchers
  22. Custom matchers • Help communicate test intention • Abstract assertion

    logic in case standard matchers are not enough • Are reusable and save time in large projects • You may have a custom message to be more specific about test failure
  23. Custom matchers • Help communicate test intention • Abstract assertion

    logic in case standard matchers are not enough • Are reusable and save time in large projects • You may have a custom message to be more specific about test failure
  24. Custom matchers @Test public void shouldHaveIsbnGenerated() { Book book =

    new Book(1l, "5555", "A book"); assertThat(book, hasIsbn("1234")); }
  25. fail() In some cases (e.g. testing exceptions) you may want

    to force test to fail if some expected situation does not happen
  26. fail() try { // do stuff... fail("Exception not thrown"); }

    catch(Exception e){ assertTrue(e.hasSomeFlag()); }
  27. fail() • Fundamentally not bad, but better use matchers for

    expected failure • Matchers help to clarify test intention • Don’t forget - expected behaviour is an opposite of a failing test
  28. Anti-pattern: The Ugly Mirror @Test public void personToStringShouldIncludeNameAndSurname() { Person

    person = new Person("Vilkas", "Pilkas"); String expected = "Person[" + person.getName() + " " + person.getSurname() + "]" assertEquals(expected, person.toString()); }
  29. Anti-pattern: The Ugly Mirror @Test public void personToStringShouldIncludeNameAndSurname() { Person

    person = new Person("Vilkas", "Pilkas"); String expected = "Person[" + person.getName() + " " + person.getSurname() + "]" assertEquals(expected, person. toString()); }
  30. Anti-pattern: The Ugly Mirror @Test public void personToStringShouldIncludeNameAndSurname() { Person

    person = new Person("Vilkas", "Pilkas"); assertEquals("Person[Vilkas Pilkas]", person.toString()); }
  31. Ignoring tests • Always use ignore/pending API from your test

    library (JUnit @Ignore) • Do not comment out or false assert your test
  32. Ignoring tests • Always use ignore/pending API from your test

    library (JUnit @Ignore) • Do not comment out or false assert your test • If you do not need a test - delete it
  33. Exceptions • If you can, use matchers instead of ◦

    @Test(expected=?) ◦ try-catch approach
  34. JUnit expected exception @Test(expected=IndexOutOfBoundsException.class) public void shouldThrowIndexOutOfBoundsException() { ArrayList emptyList

    = new ArrayList(); Object o = emptyList.get(0); } //matcher in Specs2 (Scala) server.process(None) must throwA[NothingToProccess]
  35. try and catch public void shouldThrowIndexOutOfBoundsException() { ArrayList emptyList =

    new ArrayList(); try { Object o = emptyList.get(0); fail("Should throw IndexOutOfBoundsException"); } catch(IndexOutOfBoundsException e)){ //consider asserting message! } }
  36. catch-exception lib List myList = new ArrayList(); catchException(myList).get(1); assertThat(caughtException(), allOf(

    is(IndexOutOfBoundsException.class), hasMessage("Index: 1, Size: 0"), hasNoCause() ) );
  37. Exceptions • What about ExpectedException Rule? ◦ My personal opinion

    - not that intuitive ◦ breaks arrange/act/assert flow
  38. ExpectedException rule @Rule public ExpectedException exception = ExpectedException.none(); @Test public

    void testExpectedException() { exception.expect(IllegalArgumentException.class); exception.expectMessage(containsString('Invalid age')); new Person('Vilkas', -1); } //Person constructor public Person(String name, int age) { if (age <= 0) throw new IllegalArgumentException('Invalid age:' + age); // ... }
  39. Problem public class MyService { ... public void process(LocalDate date)

    { if (date.isBefore(LocalDate.now()) { ... } } }
  40. Testing with Time • Design your system where time is

    a collaborator • Inject test specific time provider in your test ◦ constant time ◦ slow time ◦ boundary cases time
  41. Control time with Clock public class MyService { private Clock

    clock; // dependency inject ... public void process(LocalDate date) { if (date.isBefore(LocalDate.now(clock)) { ... } } }
  42. Collections • Most of the time you want to assert

    on collection content • Prefer exact content matching
  43. Collections • Most of the time you want to assert

    on collection content • Prefer exact content matching • Avoid incomplete assertions
  44. Collections • Most of the time you want to assert

    on collection content • Prefer exact content matching • Avoid incomplete assertions • Do not sort just because it is easier to assert!
  45. Collections • Most of the time you want to assert

    on collection content • Prefer exact content matching • Avoid incomplete assertions • Do not sort just because it is easier to assert! • Multiple assertions are worse than single content assertion
  46. Collections • Most of the time you want to assert

    on collection content • Prefer exact content matching • Avoid incomplete assertions • Do not sort just because it is easier to assert! • Multiple assertions are worse than single content assertion • Unless you want to say something important in your test!
  47. Collections • Most of the time you want to assert

    on collection content • Prefer exact content matching • Avoid incomplete assertions • Do not sort just because it is easier to assert! • Multiple assertions are worse than single content assertion • Unless you want to say something important in your test! • Use matchers!
  48. Collections • Most of the time you want to assert

    on collection content • Prefer exact content matching • Avoid incomplete assertions • Do not sort just because it is easier to assert! • Multiple assertions are worse than single content assertion • Unless you want to say something important in your test! • Use matchers!
  49. Random values in tests • Most of the time you

    do not want it • Unless you depend on randomness a lot (eg. password generation*) *Thanks to Aleksandar Tomovski for a good example
  50. Random values in tests • Most of the time you

    do not want it • Unless you depend on randomness a lot • Use property based testing (which is also hard)
  51. Random values in tests • Most of the time you

    do not want it • Unless you depend on randomness a lot • Use property based testing (which is also hard) • Do not make dummy values random
  52. Generate Multiple Test Cases • Quality over quantity • Think

    of boundary cases, that you may want to detect with random test
  53. Generate Multiple Test Cases • Quality over quantity • Think

    of boundary cases, that you may want to detect with random test • Use parameterized tests
  54. Generate Multiple Test Cases • Quality over quantity • Think

    of boundary cases, that you may want to detect with random test • Use parameterized tests • Random is hard to repeat
  55. Generate Multiple Test Cases • Quality over quantity • Think

    of boundary cases, that you may want to detect with random test • Use parameterized tests • Random is hard to repeat • Flickering tests
  56. How many assertions per test? • Unit test - one

    assertion per test. Must be clear and readable • Proper test should fail for exactly one reason • End to end - best case one assertion per test, but more are allowed • Consider custom matchers
  57. How many assertions per test? • Unit test - one

    assertion per test. Must be clear and readable • Proper test should fail for exactly one reason • End to end - best case one assertion per test, but more are allowed • Consider custom matchers
  58. What can be better in this test [pseudocode] @Test shouldRetrieveUserByLogin()

    { String userJson = "{\"username\": \"vaidas\"}"; HttpRequest post = new Post("https://localhost:8080/users", userJson); HttpResponse postResp = HttpClient().execute(post); assertThat(postResp.status, is(200)); HttpRequest get = new Get("https://localhost:8080/users/vaidas"); HttpResponse getResp = HttpClient().execute(get); User user = mapper.readValue(getResp, User.class); assertThat(username, is("vaidas")); }
  59. Decoupling from low level details [pseudocode] @Test shouldRetrieveUserByUsername() { CreateUserResponse

    createResp = aCreateUserRequest().withUsername("vaidas").execute(); assertThat(createResp, hasStatus(OK)); GetUserResponse getResp = aGetUserRequest().withUsername("vaidas").execute(); assertThat(getResp, allOf(hasStatus(OK), hasUsername("vaidas"))); }
  60. Thoughts on end-to-end testing • What is real e2e? REST

    vs. UI • Testing by using system itself • Vs. using low level DB driver to verify data persistence
  61. Comments in Test code • Fundamentally good option to explain

    complicated parts, but: • better use good method naming • custom matcher • do less, so that intention is clear • comments are not so bad in isolated well named methods