SlideShare a Scribd company logo
1 of 89
Download to read offline
Bad Tests, Good Tests

Tomek Kaczanowski
Tomek Kaczanowski

•   Developer
•   Team lead
•   Blogger
     • http://kaczanowscy.pl/tomek
•   Book author
     • http://practicalunittesting.com

•   Working at CodeWise (Krakow)
      ...we are hiring, wanna join us?
Before we begin

•   Most of the examples are real but:
      Obfuscated
             to protect the innocents
         Truncated
             imagine much more complex domain objects
•   Asking questions is allowed
      ...but being smarter than me is not ;)
A little bit of history
Before we begin

•   The tests were written in 2004-2006.
•   No automation, no CI.
•   Some tests do not compile.
•   In some tests you can read a comment that "WARNING:
    test requires the divide param to be set to 20" but the
    code is so ugly, that there is no way to inject this value.
•   Some test data are available in form of serialized objects
    (*.ser) that can not be currently deserialized, because
    the classes have changed.
•   The project is now in maintenance.

                           Courtesy of Bartosz http://twitter.com/#!/bocytko
We don't need no stinkin' asserts!
public void testAddChunks() {
      System.out.println("*************************************");
      System.out.println("testAddChunks() ... ");
      ChunkMap cm = new ChunkMap(3);
      cm.addChunk(new Chunk("chunk"));

     List testList = cm.getChunks("chunk",null);

     if (testList.isEmpty())
           fail("there should be at least one list!");
     Chunk chunk = cm.getActualChunk("chunk",null);
     if (chunk.getElements().isEmpty())
           fail("there should be at least one element!");
     if (cm.getFinalChunkNr() != 1)
           fail("there should be at least one chunk!");
     // iterate actual chunk
     for (Iterator it = chunk.getElements().iterator();
                             it.hasNext();) {
           Element element = (Element) it.next();
           System.out.println("Element: " + element);
     }
     showChunks(cm);
     System.out.println("testAddChunks() OK ");
}
Success is not an option...

  /**
    * Method testFailure.
    */
  public void testFailure() {
       try {
           Message message = new Message(null,true);
           fail();
       } catch(Exception ex) {
           ExceptionHandler.log(ExceptionLevel.ANY,ex);
           fail();
       }
  }
What has happened? Well, it failed...
public void testSimple() {
      IData data = null;
      IFormat format = null;
      LinkedList<String> attr = new LinkedList<String>();
      attr.add("A");
      attr.add("B");

        try {
              format = new SimpleFormat("A");
              data.setAmount(Amount.TEN);
              data.setAttributes(attr);
              IResult result = format.execute();
              System.out.println(result.size());
              Iterator iter = result.iterator();
              while (iter.hasNext()) {
                  IResult r = (IResult) iter.next();
                  System.out.println(r.getMessage());
                  ...
              }
        } catch (Exception e) {
              fail();
    }
}
What has happened? Well, it failed...
public void testSimple() {
      IData data = null;
      IFormat format = null;
      LinkedList<String> attr = new LinkedList<String>();
      attr.add("A");
      attr.add("B");                       data is still null here.
                                                Ready or not, NPE is coming.
        try {
              format = new SimpleFormat("A");
              data.setAmount(Amount.TEN);
              data.setAttributes(attr);
              IResult result = format.execute();
              System.out.println(result.size());
              Iterator iter = result.iterator();
              while (iter.hasNext()) {
                  IResult r = (IResult) iter.next();
                  System.out.println(r.getMessage());
                  ...
              }
        } catch (Exception e) {
              fail();
    }
}
Talk to me
//wait for messages
do {
     input = "";
     try {
         System.out.print(">");
         read = System.in.read(buf);
         //convert characters to string
         input = new String(buf, 0, read - newline.length());
         System.out.println(input);

        if (input.equals("end") || input.equals("exit")
            || input.equals("stop") || input.equals("quit")) {
                System.out.println("Terminating Test please wait...");
                System.out.println("******* Test terminated *******");
                toStop = true;
        }
        else {
            System.out.println("Commands:" + newline + "'end',
            'exit', 'stop' or 'quit' terminates this test ");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
} while (!toStop);
Tests are boring – let us autogenerate them!
/**                                                           protected void tearDown() throws Exception {
* Generated by JUnitDoclet, a tool provided by                  // JUnitDoclet begin method testcase.tearDown
* ObjectFab GmbH under LGPL.                                    adapter = null;
* Please see www.junitdoclet.org, www.gnu.org                   super.tearDown();
* and www.objectfab.de for informations about                   // JUnitDoclet end method testcase.tearDown
* the tool, the licence and the authors.
*/                                                            public void testMain() throws Exception {
public class AdapterTest                                        // JUnitDoclet begin method testMain
// JUnitDoclet begin extends_implements                         Adapter.main(new String [] {"ADAPTER"});
extends TestCase                                                // JUnitDoclet end method testMain
// JUnitDoclet end extends_implements                         }
{
   // JUnitDoclet begin class
   Adapter adapter = null;                                    /**
   // JUnitDoclet end class                                   * JUnitDoclet moves marker to this method, if there is not match
                                                              * for them in the regenerated code and if the marker is not empty.
  public AdapterTest(String name) {                           * This way, no test gets lost when regenerating after renaming.
    // JUnitDoclet begin method AdapterTest                   * Method testVault is supposed to be empty.
    super(name);                                              */
    // JUnitDoclet end method AdapterTest                     public void testVault() throws Exception {
  }                                                              // JUnitDoclet begin method testcase.testVault
                                                                 // JUnitDoclet end method testcase.testVault
  public Adapter createInstance() throws Exception {          }
    // JUnitDoclet begin method testcase.createInstance
    return new Adapter();                                     public static void main(String[] args) {
    // JUnitDoclet end method testcase.createInstance           // JUnitDoclet begin method testcase.main
  }                                                             junit.textui.TestRunner.run(AdapterTest.class);
                                                                // JUnitDoclet end method testcase.main
  protected void setUp() throws Exception {                   }
    // JUnitDoclet begin method testcase.setUp            }
    super.setUp();
    adapter = createInstance();
    // JUnitDoclet end method testcase.setUp
  }
Tests are boring – let us autogenerate them!
public void testSetGetTimestamp() throws Exception {
      // JUnitDoclet begin method setTimestamp getTimestamp
    java.util.Calendar[] tests = {new GregorianCalendar(), null};

        for (int i = 0; i < tests.length; i++) {
            adapter.setTimestamp(tests[i]);
            assertEquals(tests[i], adapter.getTimestamp());
        }
        // JUnitDoclet end method setTimestamp getTimestamp
    }

public void testSetGetParam() throws Exception {
    // JUnitDoclet begin method setParam getParam
    String[] tests = {"a", "aaa", "---", "23121313", "", null};

        for (int i = 0; i < tests.length; i++) {
            adapter.setParam(tests[i]);
            assertEquals(tests[i], adapter.getParam());
        }
        // JUnitDoclet end method setParam getParam
}
Conclusions

•   Automation!
     •  Running
     •  Verification
•   Tests are to be written not generated
•   You should be informed why your test failed
•   Master your tools
       …at least learn the basics!
Few words about tests
Why bother with tests?


•   System works as expected



•   Changes do not hurt



•   Documentation



                               http://twitter.com/#!/devops_borat
Tests help to achieve quality




                     Not sure when I saw this picture – probably
                     in GOOS?
What happens if we do it wrong?
•   Angry clients
•   Depressed developers




                            http://www.joshcanhelp.com
When I started out with unit tests, I was
enthralled with the promise of ease and
security that they would bring to my
projects. In practice, however, the
theory of sustainable software through
unit tests started to break down. This
difficulty continued to build up, until I
finally threw my head back in anger and
declared that "Unit Tests have become
more trouble than they are worth."
               Llewellyn Falco and Michael Kennedy, Develop Mentor August 09
http://chrispiascik.com/daily-drawings/express-yourself/
The worst kind of tests
No smoke without tests
class SystemAdminSmokeTest extends GroovyTestCase {

void testSmoke() {
// do not remove below code
// def ds = new org.h2.jdbcx.JdbcDataSource(
//     URL: 'jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=Oracle',
//     user: 'sa', password: '')
//
//     def jpaProperties = new Properties()
//     jpaProperties.setProperty(
//                'hibernate.cache.use_second_level_cache', 'false')
//     jpaProperties.setProperty(
//                'hibernate.cache.use_query_cache', 'false')
//
//     def emf = new LocalContainerEntityManagerFactoryBean(
//        dataSource: ds, persistenceUnitName: 'my-domain',
//        jpaVendorAdapter: new HibernateJpaVendorAdapter(
//            database: Database.H2, showSql: true,
//            generateDdl: true), jpaProperties: jpaProperties)

// some more code below, all commented out :(
}
Let's follow the leader!

                  @Test
                  public class ExampleTest {

                       public void testExample() {
                            assertTrue(true);
                       }
                  }
Uh-oh, I feel lonely...

                   @Test
                   public class ExampleTest {

                          public void testExample() {
                               assertTrue(true);
                          }
                   }
Flickering tests
Asking for troubles...

LoggingPropertyConfigurator configurator
       = mock(LoggingPropertyConfigurator.class);
BaseServletContextListener baseServletContextListener
       = new BaseServletContextListener(configurator);
                                                                Should load some
@Test public void shouldLoadConfigProperties() {                default config
       baseServletContextListener.contextInitialized();
       verify(configurator).configure(any(Properties.class));
}


@Test(expected = LoggingInitialisationException.class)            Should load this
public void shouldThrowLoggingException() {                       specific file

       System.setProperty("logConfig", "nonExistingFile");
       baseServletContextListener.contextInitialized();
}
Lets mock!
Mock'em All!
public String getUrl(User user, String timestamp) {
       String name=user.getFullName();
       String url=baseUrl
              +"name="+URLEncoder.encode(name, "UTF-8")
              +"&timestamp="+timestamp;
       return url;
}

public String getUrl(User user) {
       Date date=new Date();
       Long time=(date.getTime()/1000); //convert ms to seconds
       String timestamp=time.toString();
       return getUrl(user, timestamp);
}
Mock'em All!
public String getUrl(User user, String timestamp) {
       String name=user.getFullName();
       String url=baseUrl
              +"name="+URLEncoder.encode(name, "UTF-8")
              +"&timestamp="+timestamp;
       return url;
}

public String getUrl(User user) {
       Date date=new Date();
       Long time=(date.getTime()/1000); //convert ms to seconds
       String timestamp=time.toString();
       return getUrl(user, timestamp);
}

                      @Test
                      public void shouldUseTimestampMethod() {
                          //given
                          Util spyUtil = spy(util);

                           //when
                           spyUtil.getUrl(user);

                           //then
                           verify(spyUtil).getUrl(eq(user), anyString());
                      }
Use the front door

@Test
public void shouldAddTimestampToGeneratedUrl() {
        //given
        util = new ....
        TimeProvider timeProvider = mock(TimeProvider.class);
        when(timeProvider.getTime()).thenReturn("12345");
        util.set(timeProvider);


        //when
        String url = util.getUrl(user);


        //then
        assertThat(url).contains("timestamp=12345");
}
Mock'em All!
@Test
public void shouldAddTimeZoneToModelAndView() {
    //given
    UserFacade userFacade = mock(UserFacade.class);
    ModelAndView modelAndView = mock(ModelAndView.class);
    given(userFacade.getTimezone()).willReturn("timezone X");


    //when
    new UserDataInterceptor(userFacade)
        .postHandle(null, null, null, modelAndView);


    //then
    verify(modelAndView).addObject("timezone", "timezone X");
}
Use the front door
@Test
public void shouldAddTimeZoneToModelAndView() {
    //given
    UserFacade userFacade = mock(UserFacade.class);
    ModelAndView modelAndView = new ModelAndView();
    given(userFacade.getTimezone()).willReturn("timezone X");


    //when
    new UserDataInterceptor(userFacade)
        .postHandle(null, null, null, modelAndView);


    //then
    assertThat(modelAndView).constains("timezone", "timezone X");
}
Single Responsibility Principle
SRP for tests

A test should have one and only one reason to fail.
Testing two things at once
@DataProvider
public Object[][] data() {
       return new Object[][] { {"48", true}, {"+48", true},
              {"++48", true}, {"+48503", true}, {"+4", false},
              {"++4", false}, {"", false},
              {null, false}, {" ", false}, };
}

@Test(dataProvider = "data")
public void testQueryVerification(String query, boolean expected) {
       assertEquals(expected, FieldVerifier.isValidQuery(query));
}
Testing two things at once
@DataProvider
public Object[][] data() {
       return new Object[][] { {"48", true}, {"+48", true},            Data
              {"++48", true}, {"+48503", true}, {"+4", false},
              {"++4", false}, {"", false},
              {null, false}, {" ", false}, };
}

@Test(dataProvider = "data")                                      Algorithm   / Logic
public void testQueryVerification(String query, boolean expected) {
       assertEquals(expected, FieldVerifier.isValidQuery(query));
}
Testing two things at once
@DataProvider
public Object[][] data() {
       return new Object[][] { {"48", true}, {"+48", true},
              {"++48", true}, {"+48503", true}, {"+4", false},
              {"++4", false}, {"", false},
              {null, false}, {" ", false}, };
}

@Test(dataProvider = "data")
public void testQueryVerification(String query, boolean expected) {
       assertEquals(expected, FieldVerifier.isValidQuery(query));
}

          testQueryVerification1() {
              assertEquals(true, FieldVerifier.isValidQuery(„48”));
          }
          testQueryVerification2() {
              assertEquals(true, FieldVerifier.isValidQuery(„+48”));
          }
          testQueryVerification3() {
              assertEquals(true, FieldVerifier.isValidQuery(„++48”));
          }
          testQueryVerification4() {
              assertEquals(true, FieldVerifier.isValidQuery(„+48503”));
          }
          ...
Testing two things at once
@DataProvider
public Object[][] data() {
     return new Object[][] { {"48", true}, {"+48", true},
           {"++48", true}, {"+48503", true}, {"+4", false},
           {"++4", false}, {"", false},
           {null, false}, {"   ", false}, };
}


@Test(dataProvider = "data")
public void testQueryVerification(String query, boolean expected) {
     assertEquals(expected, FieldVerifier.isValidQuery(query));
}
Concentrate on one feature
@DataProvider
public Object[][] validQueries() {
  return new Object[][] { {"48"}, {"48123"},
       {"+48"}, {"++48"}, {"+48503"}};
}

@Test(dataProvider = "validQueries")
public void shouldRecognizeValidQueries(
        String validQuery) {
  assertTrue(FieldVerifier.isValidQuery(validQuery));
}


@DataProvider
public Object[][] invalidQueries() {
  return new Object[][] {
    {"+4"}, {"++4"},
    {""}, {null}, {" "} };
}

@Test(dataProvider = "invalidQueries")
public void shouldRejectInvalidQueries(
        String invalidQuery) {
  assertFalse(FieldVerifier.isValidQuery(invalidQuery));
}
Are you satisfied?
Happy path

testSum() {
     assertEquals(Math.sum(2,2), 4);
}




                          http://mw2.google.com/mw-panoramio/photos/medium/68775332.jpg
Happy paths are for wimps

2 + 2
2 + -2
2 + -5
0 + 2
2 + 0
Integer.MAX_VALUE + something
etc.




                                http://kidskidskids.tumblr.com/post/1145294997
Avoiding happy paths

Start with one:
testSum() {
      assertEquals(Math.sum(2,2), 4);
}

Do the simplest thing that works:
sum(int x, int y) {
      return 4;
}


And then listen to your code.
Because it tells you something.         http://kidskidskids.tumblr.com/post/1145294997
Avoiding happy paths

 sum(int x, int y) {
      return 4;
 }


You moron!
Your test is so pathetic,
that I can make it pass
by doing such a silly thing.
Try harder!
                          http://looneytunes09.files.wordpress.com/2010/07/lisa-yell.gif
Readability is the king
Who the heck is “user_2” ?

@DataProvider
public static Object[][] usersPermissions() {
    return new Object[][]{
         {"user_1", Permission.READ},
         {"user_1", Permission.WRITE},
         {"user_1", Permission.REMOVE},
         {"user_2", Permission.WRITE},
         {"user_2", Permission.READ},
         {"user_3", Permission.READ}
    };
}
Ah, logged user can read and write...

@DataProvider
public static Object[][] usersPermissions() {
    return new Object[][]{
         {ADMIN, Permission.READ},
         {ADMIN, Permission.WRITE},
         {ADMIN, Permission.REMOVE},
         {LOGGED, Permission.WRITE},
         {LOGGED, Permission.READ},
         {GUEST, Permission.READ}
    };
}
Do not make me learn the API!

  server = new MockServer(responseMap, true,
              new URL(SERVER_ROOT).getPort(), false);
Do not make me learn the API!

   server = new MockServer(responseMap, true,
               new URL(SERVER_ROOT).getPort(), false);




private static final boolean RESPONSE_IS_A_FILE = true;
private static final boolean NO_SSL = false;


server = new MockServer(responseMap, RESPONSE_IS_A_FILE,
                new URL(SERVER_ROOT).getPort(), NO_SSL);
Do not make me learn the API!

  server = new MockServer(responseMap, true,
              new URL(SERVER_ROOT).getPort(), false);




         server = new MockServerBuilder()
                   .withResponse(responseMap)
                   .withResponseType(FILE)
                   .withUrl(SERVER_ROOT)
                   .withoutSsl().create();
What is really important?
What is really important?
@DataProvider
public Object[][] snapshotArtifacts() {
      return new Object[][]{
                {"a", "b", "2.2-SNAPSHOT", Artifact.JAR },
                {"c", "d", "2.2.4.6-SNAPSHOT", Artifact.JAR},
                {"e", "f", "2-SNAPSHOT", Artifact.JAR}
      };
}
@Test(dataProvider = "snapshotArtifacts")
public void shouldRecognizeSnapshots(
      String groupId, String artifactId,
      String version, Type type) {
      Artifact artifact
           = new Artifact(groupId, artifactId, version, type);
      assertThat(artifact.isSnapshot()).isTrue();
}
Only version matters
@DataProvider
public Object[][] snapshotVersions() {
      return new Object[][]{
                {"2.2-SNAPSHOT"},
                {"2.2.4.6-SNAPSHOT"},
                {"2-SNAPSHOT"}
      };
}
@Test(dataProvider = "snapshotVersions")
public void shouldRecognizeSnapshots(String version) {
      Artifact artifact
           = new Artifact(VALID_GROUP, VALID_ARTIFACT_ID,
                version, VALID_TYPE);
      assertThat(artifact.isSnapshot()).isTrue();
}
Test method names
Test methods names are important

•   testFindTransactionsToAutoCharge()
•   testSystemSuccess()
•   testOperation()
Test methods names are important

•   testFindTransactionsToAutoCharge()
•   testSystemSuccess()
•   testOperation()


        @Test
        public void testOperation() {
            configureRequest("/validate")
            rc = new RequestContext(parser, request)
            assert rc.getConnector() == null
            assert rc.getOperation().equals("validate")
        }
“should” is better than “test”


•   testOperation()
•   testQuery()
•   testConstructor()
•   testFindUsersWithFilter()


•   shouldRejectInvalidRequests()
•   shouldSaveNewUserToDatabase()
•   constructorShouldFailWithNegativePrice()
•   shouldReturnOnlyUsersWithGivenName()
“should” is better than “test”

•    Starting test method names
     with “should” steers you in
     the right direction.
                                       http://jochopra.blogspot.com/




•    “test” prefix makes your test
     method a limitless bag
     where you throw everything
     worth testing


                                     http://www.greenerideal.com/
Test methods names are important
@Test
public void testQuery(){
    when(q.getResultList()).thenReturn(null);
    assertNull(dao.findByQuery(Transaction.class, q, false));
    assertNull(dao.findByQuery(Operator.class, q, false));
    assertNull(dao.findByQuery(null, null, false));

    List result = new LinkedList();
    when(q.getResultList()).thenReturn(result);
    assertEquals(dao.findByQuery(Transaction.class, q, false), result);
    assertEquals(dao.findByQuery(Operator.class, q, false), result);
    assertEquals(dao.findByQuery(null, null, false), null);

    when(q.getSingleResult()).thenReturn(null);
    assertEquals(dao.findByQuery(Transaction.class, q, true).size(), 0);
    assertEquals(dao.findByQuery(Operator.class, q, true).size(), 0);
    assertEquals(dao.findByQuery(null, null, true), null);

    when(q.getSingleResult()).thenReturn(t);
    assertSame(dao.findByQuery(Transaction.class, q, true).get(0), t);
    when(q.getSingleResult()).thenReturn(o);
    assertSame(dao.findByQuery(Operator.class, q, true).get(0), o);
    when(q.getSingleResult()).thenReturn(null);
    assertSame(dao.findByQuery(null, null, true), null);
}
Assertions
Asserting using private methods

@Test
public void testChargeInRetryingState() throws Exception {
    // given
        TxDTO request = createTxDTO(RequestType.CHARGE);
        AndroidTransaction androidTransaction = ...


    // when
    final TxDTO txDTO = processor.processRequest(request);


    // then
    assertState(request, androidTransaction,
             CHARGED, CHARGE_PENDING, AS_ANDROID_TX_STATE,
        ClientMessage.SUCCESS, ResultCode.SUCCESS);
}
Matchers vs. private methods
assertState(TxDTO txDTO, AndroidTransaction androidTransaction,
              AndroidTransactionState expectedAndroidState,
       AndroidTransactionState expectedPreviousAndroidState,
       ExtendedState expectedState,
       String expectedClientStatus,
              ResultCode expectedRequestResultCode) {
    final List<AndroidTransactionStep> steps
              = new ArrayList<>(androidTransaction.getTransactionSteps());
    final boolean checkPreviousStep = expectedAndroidState != null;
    assertTrue(steps.size() >= (checkPreviousStep ? 3 : 2));

     if (checkPreviousStep) {
         AndroidTransactionStep lastStep = steps.get(steps.size() - 2);
         assertEquals(lastStep.getTransactionState(),
                     expectedPreviousAndroidState);
     }

     final AndroidTransactionStep lastStep = steps.get(steps.size() - 1);
     assertEquals(lastStep.getTransactionState(), expectedAndroidState);
     assertEquals(lastStep.getMessage(), expectedClientStatus);

     assertEquals(txDTO.getResultCode(), expectedRequestResultCode);
     assertEquals(androidTransaction.getState(), expectedAndroidState);
     assertEquals(androidTransaction.getExtendedState(), expectedState);

     if (expectedClientStatus == null) {
         verifyZeroInteractions(client);
     }
}
Matchers vs. private methods

@Test
public void testChargeInRetryingState() throws Exception {
    // given
    TxDTO request = createTxDTO(CHARGE);
    AndroidTransaction androidTransaction = ...
    // when
    final TxDTO txDTO = processor.processRequest(request);
    // then
    assertThat(androidTransaction).hasState(CHARGED)
               .hasMessage(ClientMessage.SUCCESS)
               .hasPreviousState(CHARGE_PENDING)
               .hasExtendedState(null);
    assertEquals(txDTO.getResultCode(),
               ResultCode.SUCCESS);
}
Assertion part is freaking huge
public void shouldPreDeployApplication() {
        // given
        Artifact artifact = mock(Artifact.class);
        when(artifact.getFileName()).thenReturn("war-artifact-2.0.war");
        ServerConfiguration config
                = new ServerConfiguration(ADDRESS, USER, KEY_FILE, TOMCAT_PATH, TEMP_PATH);
        Tomcat tomcat = new Tomcat(HTTP_TOMCAT_URL, config);
        String destDir = new File(".").getCanonicalPath() + SLASH + "target" + SLASH;
        new File(destDir).mkdirs();

       // when
       tomcat.preDeploy(artifact, new FakeWar(WAR_FILE_LENGTH));

       //then
       JSch jsch = new JSch();
       jsch.addIdentity(KEY_FILE);
       Session session = jsch.getSession(USER, ADDRESS, 22);
       session.setConfig("StrictHostKeyChecking", "no");
       session.connect();

       Channel channel = session.openChannel("sftp");
       session.setServerAliveInterval(92000);
       channel.connect();
       ChannelSftp sftpChannel = (ChannelSftp) channel;

       sftpChannel.get(TEMP_PATH + SLASH + artifact.getFileName(), destDir);
       sftpChannel.exit();

       session.disconnect();

       File downloadedFile = new File(destDir, artifact.getFileName());

       assertThat(downloadedFile).exists().hasSize(WAR_FILE_LENGTH);
}
Just say it

public void shouldPreDeployApplication() {
     Artifact artifact = mock(Artifact.class);
     when(artifact.getFileName())
          .thenReturn(ARTIFACT_FILE_NAME);
     ServerConfiguration config
          = new ServerConfiguration(ADDRESS, USER,
                     KEY_FILE, TOMCAT_PATH, TEMP_PATH);
     Tomcat tomcat = new Tomcat(HTTP_TOMCAT_URL, config);


     tomcat.preDeploy(artifact, new FakeWar(WAR_FILE_LENGTH));


     SSHServerAssert.assertThat(ARTIFACT_FILE_NAME)
          .existsOnServer(config).hasSize(WAR_FILE_LENGTH);
}
What is asserted?

@Test
public void testCompile_32Bit_FakeSourceFile() {
        CompilerSupport _32BitCompilerSupport
             = CompilerSupportFactory.getDefault32BitCompilerSupport();
        testCompile_FakeSourceFile(_32BitCompilerSupport);
}
What is asserted?

@Test
public void testCompile_32Bit_FakeSourceFile() {
        CompilerSupport _32BitCompilerSupport
             = CompilerSupportFactory.getDefault32BitCompilerSupport();
        testCompile_FakeSourceFile(_32BitCompilerSupport);
}


private void testCompile_FakeSourceFile(
                         CompilerSupport compilerSupport) {
        compiledFiles
             = compilerSupport.compile(new File[] { new File("fake") });
        assertThat(compiledFiles, is(emptyArray()));
}
Asserting everything

public void invalidTxShouldBeCanceled() {
    String fileContent =
        FileUtils.getContentOfFile("response.csv");
    assertTrue(fileContent.contains(
        "CANCEL,123,123cancel,billing_id_123_cancel,SUCCESS,"));
}
Asserting everything

public void invalidTxShouldBeCanceled() {
    String fileContent =
        FileUtils.getContentOfFile("response.csv");
    assertTrue(fileContent.contains(
        "CANCEL,123,123cancel,billing_id_123_cancel,SUCCESS,"));
}



public void invalidTxShouldBeCanceled() {
    String fileContent =
        FileUtils.getContentOfFile("response.csv");
    TxDTOAssert.assertThat(fileContent)
          .hasTransaction("123cancel").withResultCode(SUCCESS);
}
Know your tool
Expected exceptions

@Test(expectedExceptions = SmsException.class)
public void shouldThrowException() throws SmsException {
    try {
        String s = gutExtractor.extractGut(„invalid gut”);
        System.out.println(s);
    } catch (SmsException e) {
        e.printStackTrace();
        throw e;
    }
}
Expected exceptions

@Test(expectedExceptions = SmsException.class)
public void shouldThrowException() throws SmsException {
    String s = gutExtractor.extractGut(„invalid gut”);
}
Expected exceptions (with catch-exception)

@Test
public void shouldThrowException() throws SmsException {


        when(gutExtractor.extractGut(„invalid gut”));


        then(caughtException())
          .isInstanceOf(SmsException.class)
          .hasMessage("Invalid gut")
          .hasNoCause();
}


                                  http://code.google.com/p/catch-exception/
Running SUT's code concurrently

@Test(threadPoolSize = 3, invocationCount = 10)
public void testServer() {
    // this method will be run in parallel by 3 thread
    // 10 invocations (in total)
}
Dependent test methods

@Test
public void shouldConnectToDB() {
    // verifying that you can
    // estabilish a connection with DB
}



@Test(dependsOnMethods = „shouldConnectToDB”)
public void should…() {
    // some operations on DB
}
Know your tools
•   Unit testing framework                Additional libraries
         Use of temporary file rule            Hamcrest, FEST, Mockito,
         Listeners                              catch-exception, …
         Concurrency                  •   Build tool
         @Before/@After                       Parallel execution

         Parametrized tests              CI

         Test dependencies            •   IDE
                                                Templates
                                                Shortcuts
What do you really want to test?
What do you really want to test?

  @Test
  public void shouldAddAUser() {
      User user = new User();
      userService.save(user);
      assertEquals(dao.getNbOfUsers(), 1);
  }
You wanted to see that the number increased

  @Test
  public void shouldAddAUser() {
          Int nb = dao.getNbOfUsers();
          User user = new User();
          userService.save(user);
          assertEquals(dao.getNbOfUsers(), nb + 1);
  }
Random
Doing it wrong
public void myTest() {
    SomeObject obj = new SomeObject(
         a, b, c, productCode());
    // testing of obj here
}

private String productCode(){
    String[] codes = {"Code A", "Code B",
         "Code C", "Code D"};
    int index = rand.nextInt(codes.length);
    return codes[index];
}
The dream of stronger, random-powered tests
public void myTest() {
    SomeObject obj = new SomeObject(
         randomName(), randomValue(), ....);
    // testing of obj here
}

Does it make your test stronger?
The dream of stronger, random-powered tests
public void myTest() {
    SomeObject obj = new SomeObject(
         randomName(), randomValue(), ....);
    // testing of obj here
}

Does it make your test stronger?

...or does it only bring confusion?
Test failed
Expected SomeObject(„a”, „b”, ....) but got
Expected SomeObject(„*&O*$NdlF”, „#idSLNF”, ....)
Conclusions
There is more to it


•    Integration / end-to-end tests which are not parametrized
     (so they all try to set up jetty on port 8080),
•    Tests which should be really unit, but use Spring context
     to create objects,
•    Tests with a lot of dependencies between them (a
     nightmare to maintain!),
•    Tests which run slow
•    Tests which try to cover the deficiencies of production
     code and end up being a total mess,
•    etc., etc.
Test-last? No!


• makes people not write tests at all
• makes people do only happy-testing
• tests reflect the implementation
Always TDD?

For six or eight hours spread over the next few weeks I
struggled to get the first test written and running. Writing
tests for Eclipse plug-ins is not trivial, so it’s not
surprising I had some trouble. [...] In six or eight hours
of solid programming time, I can still make significant
progress. If I’d just written some stuff and verified it by
hand, I would probably have the final answer to whether
my idea is actually worth money by now. Instead, all I
have is a complicated test that doesn’t work, a pile
of frustration, eight fewer hours in my life, and the
motivation to write another essay.
                                           Ken Beck, Just Ship, Baby
Treat tests as the first class citizens
•    do it everyday or forget about it          •   make tests readable using matchers,
•    use the right tool for the job                 builders and good names
     •   and learn to use it!                   •   test behaviour not methods
•    do not live with broken windows            •   be pragmatic about the tests you write
•    respect KISS, SRP, DRY (?)                     •   TDD always?
•    write good code, and you will also write       •   what is the best way to test it?
     good tests                                         unit/integration/end-to-end ?
     •   or rather write good tests and you     •   automate!
         will get good code for free            •   always concentrate on what is worth
•    code review your tests                         testing
•    do more than happy path testing                •   ask yourself questions like: 'is it
                                                        really important that X should send
•    do not make the reader learn the API,              message Y to Z?'
     make it obvious
                                                •   use the front door – state testing before
•    bad names lead to bad tests                    interaction testing (mockc)
…questions?
…rants?
…hate speeches?
…any other forms of expressing your ego?




P.S. If you have some „interesting” tests, I would be happy to see them.
Send them to me, please!
Thank you!
Thank you for watching these
slides! You can learn more about
wirting high quality tests by
reading my book – „Practical Unit
Testing with TestNG and
Mockito”.

Regards,
Tomek Kaczanowski
http://practicalunittesting.com

More Related Content

What's hot

Mocking in Java with Mockito
Mocking in Java with MockitoMocking in Java with Mockito
Mocking in Java with Mockito
Richard Paul
 
Advanced junit and mockito
Advanced junit and mockitoAdvanced junit and mockito
Advanced junit and mockito
Mathieu Carbou
 

What's hot (20)

Junit
JunitJunit
Junit
 
Junit
JunitJunit
Junit
 
Unit testing with java
Unit testing with javaUnit testing with java
Unit testing with java
 
Testing with Junit4
Testing with Junit4Testing with Junit4
Testing with Junit4
 
An introduction to Google test framework
An introduction to Google test frameworkAn introduction to Google test framework
An introduction to Google test framework
 
Understanding JavaScript Testing
Understanding JavaScript TestingUnderstanding JavaScript Testing
Understanding JavaScript Testing
 
JUnit- A Unit Testing Framework
JUnit- A Unit Testing FrameworkJUnit- A Unit Testing Framework
JUnit- A Unit Testing Framework
 
Understanding JavaScript Testing
Understanding JavaScript TestingUnderstanding JavaScript Testing
Understanding JavaScript Testing
 
xUnit Style Database Testing
xUnit Style Database TestingxUnit Style Database Testing
xUnit Style Database Testing
 
Mocking in Java with Mockito
Mocking in Java with MockitoMocking in Java with Mockito
Mocking in Java with Mockito
 
Advanced junit and mockito
Advanced junit and mockitoAdvanced junit and mockito
Advanced junit and mockito
 
C++ Unit Test with Google Testing Framework
C++ Unit Test with Google Testing FrameworkC++ Unit Test with Google Testing Framework
C++ Unit Test with Google Testing Framework
 
Junit and testNG
Junit and testNGJunit and testNG
Junit and testNG
 
Test driven development - JUnit basics and best practices
Test driven development - JUnit basics and best practicesTest driven development - JUnit basics and best practices
Test driven development - JUnit basics and best practices
 
JUNit Presentation
JUNit PresentationJUNit Presentation
JUNit Presentation
 
Unit testing with JUnit
Unit testing with JUnitUnit testing with JUnit
Unit testing with JUnit
 
Unit testing best practices with JUnit
Unit testing best practices with JUnitUnit testing best practices with JUnit
Unit testing best practices with JUnit
 
Unit testing with PHPUnit - there's life outside of TDD
Unit testing with PHPUnit - there's life outside of TDDUnit testing with PHPUnit - there's life outside of TDD
Unit testing with PHPUnit - there's life outside of TDD
 
Unit testing with Junit
Unit testing with JunitUnit testing with Junit
Unit testing with Junit
 
Introduction to Unit Testing with PHPUnit
Introduction to Unit Testing with PHPUnitIntroduction to Unit Testing with PHPUnit
Introduction to Unit Testing with PHPUnit
 

Similar to GeeCON 2012 Bad Tests, Good Tests

J unit presentation
J unit presentationJ unit presentation
J unit presentation
Priya Sharma
 
PQTimer.java A simple driver program to run timing t.docx
  PQTimer.java     A simple driver program to run timing t.docx  PQTimer.java     A simple driver program to run timing t.docx
PQTimer.java A simple driver program to run timing t.docx
joyjonna282
 
Unit testing in iOS featuring OCUnit, GHUnit & OCMock
Unit testing in iOS featuring OCUnit, GHUnit & OCMockUnit testing in iOS featuring OCUnit, GHUnit & OCMock
Unit testing in iOS featuring OCUnit, GHUnit & OCMock
Robot Media
 
Description (Part A) In this lab you will write a Queue implementati.pdf
Description (Part A) In this lab you will write a Queue implementati.pdfDescription (Part A) In this lab you will write a Queue implementati.pdf
Description (Part A) In this lab you will write a Queue implementati.pdf
rishabjain5053
 

Similar to GeeCON 2012 Bad Tests, Good Tests (20)

Confitura 2012 Bad Tests, Good Tests
Confitura 2012 Bad Tests, Good TestsConfitura 2012 Bad Tests, Good Tests
Confitura 2012 Bad Tests, Good Tests
 
2012 JDays Bad Tests Good Tests
2012 JDays Bad Tests Good Tests2012 JDays Bad Tests Good Tests
2012 JDays Bad Tests Good Tests
 
J unit presentation
J unit presentationJ unit presentation
J unit presentation
 
JUnit Presentation
JUnit PresentationJUnit Presentation
JUnit Presentation
 
How to Start Test-Driven Development in Legacy Code
How to Start Test-Driven Development in Legacy CodeHow to Start Test-Driven Development in Legacy Code
How to Start Test-Driven Development in Legacy Code
 
How to write clean tests
How to write clean testsHow to write clean tests
How to write clean tests
 
J Unit
J UnitJ Unit
J Unit
 
Junit4&testng presentation
Junit4&testng presentationJunit4&testng presentation
Junit4&testng presentation
 
Java programs
Java programsJava programs
Java programs
 
Junit With Eclipse
Junit With EclipseJunit With Eclipse
Junit With Eclipse
 
PQTimer.java A simple driver program to run timing t.docx
  PQTimer.java     A simple driver program to run timing t.docx  PQTimer.java     A simple driver program to run timing t.docx
PQTimer.java A simple driver program to run timing t.docx
 
Testing, Performance Analysis, and jQuery 1.4
Testing, Performance Analysis, and jQuery 1.4Testing, Performance Analysis, and jQuery 1.4
Testing, Performance Analysis, and jQuery 1.4
 
Pragmatic unittestingwithj unit
Pragmatic unittestingwithj unitPragmatic unittestingwithj unit
Pragmatic unittestingwithj unit
 
Presentation Unit Testing process
Presentation Unit Testing processPresentation Unit Testing process
Presentation Unit Testing process
 
Developer Test - Things to Know
Developer Test - Things to KnowDeveloper Test - Things to Know
Developer Test - Things to Know
 
Unit testing in iOS featuring OCUnit, GHUnit & OCMock
Unit testing in iOS featuring OCUnit, GHUnit & OCMockUnit testing in iOS featuring OCUnit, GHUnit & OCMock
Unit testing in iOS featuring OCUnit, GHUnit & OCMock
 
TestNG vs Junit
TestNG vs JunitTestNG vs Junit
TestNG vs Junit
 
Good Tests Bad Tests
Good Tests Bad TestsGood Tests Bad Tests
Good Tests Bad Tests
 
Google guava
Google guavaGoogle guava
Google guava
 
Description (Part A) In this lab you will write a Queue implementati.pdf
Description (Part A) In this lab you will write a Queue implementati.pdfDescription (Part A) In this lab you will write a Queue implementati.pdf
Description (Part A) In this lab you will write a Queue implementati.pdf
 

More from Tomek Kaczanowski (6)

2015 ACE! Conference slides
2015 ACE! Conference slides2015 ACE! Conference slides
2015 ACE! Conference slides
 
Grupowe podejmowanie decyzji
Grupowe podejmowanie decyzjiGrupowe podejmowanie decyzji
Grupowe podejmowanie decyzji
 
Practical Unit Testing with TestNG and Mockito
Practical Unit Testing with TestNG and MockitoPractical Unit Testing with TestNG and Mockito
Practical Unit Testing with TestNG and Mockito
 
GeeCON 2011 Who Watches The Watchmen? - On Quality Of Tests
GeeCON 2011 Who Watches The Watchmen? - On Quality Of TestsGeeCON 2011 Who Watches The Watchmen? - On Quality Of Tests
GeeCON 2011 Who Watches The Watchmen? - On Quality Of Tests
 
Convention Over Configuration - Maven 3, Polyglot Maven, Gradle and Ant
Convention Over Configuration - Maven 3, Polyglot Maven, Gradle and AntConvention Over Configuration - Maven 3, Polyglot Maven, Gradle and Ant
Convention Over Configuration - Maven 3, Polyglot Maven, Gradle and Ant
 
Gradle talk, Javarsovia 2010
Gradle talk, Javarsovia 2010Gradle talk, Javarsovia 2010
Gradle talk, Javarsovia 2010
 

Recently uploaded

Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers:  A Deep Dive into Serverless Spatial Data and FMECloud Frontiers:  A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Safe Software
 

Recently uploaded (20)

Partners Life - Insurer Innovation Award 2024
Partners Life - Insurer Innovation Award 2024Partners Life - Insurer Innovation Award 2024
Partners Life - Insurer Innovation Award 2024
 
Manulife - Insurer Innovation Award 2024
Manulife - Insurer Innovation Award 2024Manulife - Insurer Innovation Award 2024
Manulife - Insurer Innovation Award 2024
 
MINDCTI Revenue Release Quarter One 2024
MINDCTI Revenue Release Quarter One 2024MINDCTI Revenue Release Quarter One 2024
MINDCTI Revenue Release Quarter One 2024
 
A Domino Admins Adventures (Engage 2024)
A Domino Admins Adventures (Engage 2024)A Domino Admins Adventures (Engage 2024)
A Domino Admins Adventures (Engage 2024)
 
TrustArc Webinar - Stay Ahead of US State Data Privacy Law Developments
TrustArc Webinar - Stay Ahead of US State Data Privacy Law DevelopmentsTrustArc Webinar - Stay Ahead of US State Data Privacy Law Developments
TrustArc Webinar - Stay Ahead of US State Data Privacy Law Developments
 
Data Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt RobisonData Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt Robison
 
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...
Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...
 
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers:  A Deep Dive into Serverless Spatial Data and FMECloud Frontiers:  A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
 
Polkadot JAM Slides - Token2049 - By Dr. Gavin Wood
Polkadot JAM Slides - Token2049 - By Dr. Gavin WoodPolkadot JAM Slides - Token2049 - By Dr. Gavin Wood
Polkadot JAM Slides - Token2049 - By Dr. Gavin Wood
 
Exploring the Future Potential of AI-Enabled Smartphone Processors
Exploring the Future Potential of AI-Enabled Smartphone ProcessorsExploring the Future Potential of AI-Enabled Smartphone Processors
Exploring the Future Potential of AI-Enabled Smartphone Processors
 
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...
 
Artificial Intelligence Chap.5 : Uncertainty
Artificial Intelligence Chap.5 : UncertaintyArtificial Intelligence Chap.5 : Uncertainty
Artificial Intelligence Chap.5 : Uncertainty
 
Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...
 
Top 10 Most Downloaded Games on Play Store in 2024
Top 10 Most Downloaded Games on Play Store in 2024Top 10 Most Downloaded Games on Play Store in 2024
Top 10 Most Downloaded Games on Play Store in 2024
 
Understanding Discord NSFW Servers A Guide for Responsible Users.pdf
Understanding Discord NSFW Servers A Guide for Responsible Users.pdfUnderstanding Discord NSFW Servers A Guide for Responsible Users.pdf
Understanding Discord NSFW Servers A Guide for Responsible Users.pdf
 
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, AdobeApidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
 
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemkeProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
 
Strategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a FresherStrategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a Fresher
 
🐬 The future of MySQL is Postgres 🐘
🐬  The future of MySQL is Postgres   🐘🐬  The future of MySQL is Postgres   🐘
🐬 The future of MySQL is Postgres 🐘
 
The 7 Things I Know About Cyber Security After 25 Years | April 2024
The 7 Things I Know About Cyber Security After 25 Years | April 2024The 7 Things I Know About Cyber Security After 25 Years | April 2024
The 7 Things I Know About Cyber Security After 25 Years | April 2024
 

GeeCON 2012 Bad Tests, Good Tests

  • 1. Bad Tests, Good Tests Tomek Kaczanowski
  • 2. Tomek Kaczanowski • Developer • Team lead • Blogger • http://kaczanowscy.pl/tomek • Book author • http://practicalunittesting.com • Working at CodeWise (Krakow)  ...we are hiring, wanna join us?
  • 3. Before we begin • Most of the examples are real but:  Obfuscated  to protect the innocents  Truncated  imagine much more complex domain objects • Asking questions is allowed  ...but being smarter than me is not ;)
  • 4. A little bit of history
  • 5. Before we begin • The tests were written in 2004-2006. • No automation, no CI. • Some tests do not compile. • In some tests you can read a comment that "WARNING: test requires the divide param to be set to 20" but the code is so ugly, that there is no way to inject this value. • Some test data are available in form of serialized objects (*.ser) that can not be currently deserialized, because the classes have changed. • The project is now in maintenance. Courtesy of Bartosz http://twitter.com/#!/bocytko
  • 6. We don't need no stinkin' asserts! public void testAddChunks() { System.out.println("*************************************"); System.out.println("testAddChunks() ... "); ChunkMap cm = new ChunkMap(3); cm.addChunk(new Chunk("chunk")); List testList = cm.getChunks("chunk",null); if (testList.isEmpty()) fail("there should be at least one list!"); Chunk chunk = cm.getActualChunk("chunk",null); if (chunk.getElements().isEmpty()) fail("there should be at least one element!"); if (cm.getFinalChunkNr() != 1) fail("there should be at least one chunk!"); // iterate actual chunk for (Iterator it = chunk.getElements().iterator(); it.hasNext();) { Element element = (Element) it.next(); System.out.println("Element: " + element); } showChunks(cm); System.out.println("testAddChunks() OK "); }
  • 7. Success is not an option... /** * Method testFailure. */ public void testFailure() { try { Message message = new Message(null,true); fail(); } catch(Exception ex) { ExceptionHandler.log(ExceptionLevel.ANY,ex); fail(); } }
  • 8. What has happened? Well, it failed... public void testSimple() { IData data = null; IFormat format = null; LinkedList<String> attr = new LinkedList<String>(); attr.add("A"); attr.add("B"); try { format = new SimpleFormat("A"); data.setAmount(Amount.TEN); data.setAttributes(attr); IResult result = format.execute(); System.out.println(result.size()); Iterator iter = result.iterator(); while (iter.hasNext()) { IResult r = (IResult) iter.next(); System.out.println(r.getMessage()); ... } } catch (Exception e) { fail(); } }
  • 9. What has happened? Well, it failed... public void testSimple() { IData data = null; IFormat format = null; LinkedList<String> attr = new LinkedList<String>(); attr.add("A"); attr.add("B"); data is still null here. Ready or not, NPE is coming. try { format = new SimpleFormat("A"); data.setAmount(Amount.TEN); data.setAttributes(attr); IResult result = format.execute(); System.out.println(result.size()); Iterator iter = result.iterator(); while (iter.hasNext()) { IResult r = (IResult) iter.next(); System.out.println(r.getMessage()); ... } } catch (Exception e) { fail(); } }
  • 10. Talk to me //wait for messages do { input = ""; try { System.out.print(">"); read = System.in.read(buf); //convert characters to string input = new String(buf, 0, read - newline.length()); System.out.println(input); if (input.equals("end") || input.equals("exit") || input.equals("stop") || input.equals("quit")) { System.out.println("Terminating Test please wait..."); System.out.println("******* Test terminated *******"); toStop = true; } else { System.out.println("Commands:" + newline + "'end', 'exit', 'stop' or 'quit' terminates this test "); } } catch (Exception e) { e.printStackTrace(); } } while (!toStop);
  • 11. Tests are boring – let us autogenerate them! /** protected void tearDown() throws Exception { * Generated by JUnitDoclet, a tool provided by // JUnitDoclet begin method testcase.tearDown * ObjectFab GmbH under LGPL. adapter = null; * Please see www.junitdoclet.org, www.gnu.org super.tearDown(); * and www.objectfab.de for informations about // JUnitDoclet end method testcase.tearDown * the tool, the licence and the authors. */ public void testMain() throws Exception { public class AdapterTest // JUnitDoclet begin method testMain // JUnitDoclet begin extends_implements Adapter.main(new String [] {"ADAPTER"}); extends TestCase // JUnitDoclet end method testMain // JUnitDoclet end extends_implements } { // JUnitDoclet begin class Adapter adapter = null; /** // JUnitDoclet end class * JUnitDoclet moves marker to this method, if there is not match * for them in the regenerated code and if the marker is not empty. public AdapterTest(String name) { * This way, no test gets lost when regenerating after renaming. // JUnitDoclet begin method AdapterTest * Method testVault is supposed to be empty. super(name); */ // JUnitDoclet end method AdapterTest public void testVault() throws Exception { } // JUnitDoclet begin method testcase.testVault // JUnitDoclet end method testcase.testVault public Adapter createInstance() throws Exception { } // JUnitDoclet begin method testcase.createInstance return new Adapter(); public static void main(String[] args) { // JUnitDoclet end method testcase.createInstance // JUnitDoclet begin method testcase.main } junit.textui.TestRunner.run(AdapterTest.class); // JUnitDoclet end method testcase.main protected void setUp() throws Exception { } // JUnitDoclet begin method testcase.setUp } super.setUp(); adapter = createInstance(); // JUnitDoclet end method testcase.setUp }
  • 12. Tests are boring – let us autogenerate them! public void testSetGetTimestamp() throws Exception { // JUnitDoclet begin method setTimestamp getTimestamp java.util.Calendar[] tests = {new GregorianCalendar(), null}; for (int i = 0; i < tests.length; i++) { adapter.setTimestamp(tests[i]); assertEquals(tests[i], adapter.getTimestamp()); } // JUnitDoclet end method setTimestamp getTimestamp } public void testSetGetParam() throws Exception { // JUnitDoclet begin method setParam getParam String[] tests = {"a", "aaa", "---", "23121313", "", null}; for (int i = 0; i < tests.length; i++) { adapter.setParam(tests[i]); assertEquals(tests[i], adapter.getParam()); } // JUnitDoclet end method setParam getParam }
  • 13. Conclusions • Automation! • Running • Verification • Tests are to be written not generated • You should be informed why your test failed • Master your tools  …at least learn the basics!
  • 15. Why bother with tests? • System works as expected • Changes do not hurt • Documentation http://twitter.com/#!/devops_borat
  • 16. Tests help to achieve quality Not sure when I saw this picture – probably in GOOS?
  • 17. What happens if we do it wrong? • Angry clients • Depressed developers http://www.joshcanhelp.com
  • 18. When I started out with unit tests, I was enthralled with the promise of ease and security that they would bring to my projects. In practice, however, the theory of sustainable software through unit tests started to break down. This difficulty continued to build up, until I finally threw my head back in anger and declared that "Unit Tests have become more trouble than they are worth." Llewellyn Falco and Michael Kennedy, Develop Mentor August 09
  • 20. The worst kind of tests
  • 21. No smoke without tests class SystemAdminSmokeTest extends GroovyTestCase { void testSmoke() { // do not remove below code // def ds = new org.h2.jdbcx.JdbcDataSource( // URL: 'jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=Oracle', // user: 'sa', password: '') // // def jpaProperties = new Properties() // jpaProperties.setProperty( // 'hibernate.cache.use_second_level_cache', 'false') // jpaProperties.setProperty( // 'hibernate.cache.use_query_cache', 'false') // // def emf = new LocalContainerEntityManagerFactoryBean( // dataSource: ds, persistenceUnitName: 'my-domain', // jpaVendorAdapter: new HibernateJpaVendorAdapter( // database: Database.H2, showSql: true, // generateDdl: true), jpaProperties: jpaProperties) // some more code below, all commented out :( }
  • 22. Let's follow the leader! @Test public class ExampleTest { public void testExample() { assertTrue(true); } }
  • 23. Uh-oh, I feel lonely... @Test public class ExampleTest { public void testExample() { assertTrue(true); } }
  • 25. Asking for troubles... LoggingPropertyConfigurator configurator = mock(LoggingPropertyConfigurator.class); BaseServletContextListener baseServletContextListener = new BaseServletContextListener(configurator); Should load some @Test public void shouldLoadConfigProperties() { default config baseServletContextListener.contextInitialized(); verify(configurator).configure(any(Properties.class)); } @Test(expected = LoggingInitialisationException.class) Should load this public void shouldThrowLoggingException() { specific file System.setProperty("logConfig", "nonExistingFile"); baseServletContextListener.contextInitialized(); }
  • 27. Mock'em All! public String getUrl(User user, String timestamp) { String name=user.getFullName(); String url=baseUrl +"name="+URLEncoder.encode(name, "UTF-8") +"&timestamp="+timestamp; return url; } public String getUrl(User user) { Date date=new Date(); Long time=(date.getTime()/1000); //convert ms to seconds String timestamp=time.toString(); return getUrl(user, timestamp); }
  • 28. Mock'em All! public String getUrl(User user, String timestamp) { String name=user.getFullName(); String url=baseUrl +"name="+URLEncoder.encode(name, "UTF-8") +"&timestamp="+timestamp; return url; } public String getUrl(User user) { Date date=new Date(); Long time=(date.getTime()/1000); //convert ms to seconds String timestamp=time.toString(); return getUrl(user, timestamp); } @Test public void shouldUseTimestampMethod() { //given Util spyUtil = spy(util); //when spyUtil.getUrl(user); //then verify(spyUtil).getUrl(eq(user), anyString()); }
  • 29. Use the front door @Test public void shouldAddTimestampToGeneratedUrl() { //given util = new .... TimeProvider timeProvider = mock(TimeProvider.class); when(timeProvider.getTime()).thenReturn("12345"); util.set(timeProvider); //when String url = util.getUrl(user); //then assertThat(url).contains("timestamp=12345"); }
  • 30. Mock'em All! @Test public void shouldAddTimeZoneToModelAndView() { //given UserFacade userFacade = mock(UserFacade.class); ModelAndView modelAndView = mock(ModelAndView.class); given(userFacade.getTimezone()).willReturn("timezone X"); //when new UserDataInterceptor(userFacade) .postHandle(null, null, null, modelAndView); //then verify(modelAndView).addObject("timezone", "timezone X"); }
  • 31. Use the front door @Test public void shouldAddTimeZoneToModelAndView() { //given UserFacade userFacade = mock(UserFacade.class); ModelAndView modelAndView = new ModelAndView(); given(userFacade.getTimezone()).willReturn("timezone X"); //when new UserDataInterceptor(userFacade) .postHandle(null, null, null, modelAndView); //then assertThat(modelAndView).constains("timezone", "timezone X"); }
  • 33. SRP for tests A test should have one and only one reason to fail.
  • 34. Testing two things at once @DataProvider public Object[][] data() { return new Object[][] { {"48", true}, {"+48", true}, {"++48", true}, {"+48503", true}, {"+4", false}, {"++4", false}, {"", false}, {null, false}, {" ", false}, }; } @Test(dataProvider = "data") public void testQueryVerification(String query, boolean expected) { assertEquals(expected, FieldVerifier.isValidQuery(query)); }
  • 35. Testing two things at once @DataProvider public Object[][] data() { return new Object[][] { {"48", true}, {"+48", true}, Data {"++48", true}, {"+48503", true}, {"+4", false}, {"++4", false}, {"", false}, {null, false}, {" ", false}, }; } @Test(dataProvider = "data") Algorithm / Logic public void testQueryVerification(String query, boolean expected) { assertEquals(expected, FieldVerifier.isValidQuery(query)); }
  • 36. Testing two things at once @DataProvider public Object[][] data() { return new Object[][] { {"48", true}, {"+48", true}, {"++48", true}, {"+48503", true}, {"+4", false}, {"++4", false}, {"", false}, {null, false}, {" ", false}, }; } @Test(dataProvider = "data") public void testQueryVerification(String query, boolean expected) { assertEquals(expected, FieldVerifier.isValidQuery(query)); } testQueryVerification1() { assertEquals(true, FieldVerifier.isValidQuery(„48”)); } testQueryVerification2() { assertEquals(true, FieldVerifier.isValidQuery(„+48”)); } testQueryVerification3() { assertEquals(true, FieldVerifier.isValidQuery(„++48”)); } testQueryVerification4() { assertEquals(true, FieldVerifier.isValidQuery(„+48503”)); } ...
  • 37. Testing two things at once @DataProvider public Object[][] data() { return new Object[][] { {"48", true}, {"+48", true}, {"++48", true}, {"+48503", true}, {"+4", false}, {"++4", false}, {"", false}, {null, false}, {" ", false}, }; } @Test(dataProvider = "data") public void testQueryVerification(String query, boolean expected) { assertEquals(expected, FieldVerifier.isValidQuery(query)); }
  • 38. Concentrate on one feature @DataProvider public Object[][] validQueries() { return new Object[][] { {"48"}, {"48123"}, {"+48"}, {"++48"}, {"+48503"}}; } @Test(dataProvider = "validQueries") public void shouldRecognizeValidQueries( String validQuery) { assertTrue(FieldVerifier.isValidQuery(validQuery)); } @DataProvider public Object[][] invalidQueries() { return new Object[][] { {"+4"}, {"++4"}, {""}, {null}, {" "} }; } @Test(dataProvider = "invalidQueries") public void shouldRejectInvalidQueries( String invalidQuery) { assertFalse(FieldVerifier.isValidQuery(invalidQuery)); }
  • 40. Happy path testSum() { assertEquals(Math.sum(2,2), 4); } http://mw2.google.com/mw-panoramio/photos/medium/68775332.jpg
  • 41. Happy paths are for wimps 2 + 2 2 + -2 2 + -5 0 + 2 2 + 0 Integer.MAX_VALUE + something etc. http://kidskidskids.tumblr.com/post/1145294997
  • 42. Avoiding happy paths Start with one: testSum() { assertEquals(Math.sum(2,2), 4); } Do the simplest thing that works: sum(int x, int y) { return 4; } And then listen to your code. Because it tells you something. http://kidskidskids.tumblr.com/post/1145294997
  • 43. Avoiding happy paths sum(int x, int y) { return 4; } You moron! Your test is so pathetic, that I can make it pass by doing such a silly thing. Try harder! http://looneytunes09.files.wordpress.com/2010/07/lisa-yell.gif
  • 45. Who the heck is “user_2” ? @DataProvider public static Object[][] usersPermissions() { return new Object[][]{ {"user_1", Permission.READ}, {"user_1", Permission.WRITE}, {"user_1", Permission.REMOVE}, {"user_2", Permission.WRITE}, {"user_2", Permission.READ}, {"user_3", Permission.READ} }; }
  • 46. Ah, logged user can read and write... @DataProvider public static Object[][] usersPermissions() { return new Object[][]{ {ADMIN, Permission.READ}, {ADMIN, Permission.WRITE}, {ADMIN, Permission.REMOVE}, {LOGGED, Permission.WRITE}, {LOGGED, Permission.READ}, {GUEST, Permission.READ} }; }
  • 47. Do not make me learn the API! server = new MockServer(responseMap, true, new URL(SERVER_ROOT).getPort(), false);
  • 48. Do not make me learn the API! server = new MockServer(responseMap, true, new URL(SERVER_ROOT).getPort(), false); private static final boolean RESPONSE_IS_A_FILE = true; private static final boolean NO_SSL = false; server = new MockServer(responseMap, RESPONSE_IS_A_FILE, new URL(SERVER_ROOT).getPort(), NO_SSL);
  • 49. Do not make me learn the API! server = new MockServer(responseMap, true, new URL(SERVER_ROOT).getPort(), false); server = new MockServerBuilder() .withResponse(responseMap) .withResponseType(FILE) .withUrl(SERVER_ROOT) .withoutSsl().create();
  • 50. What is really important?
  • 51. What is really important? @DataProvider public Object[][] snapshotArtifacts() { return new Object[][]{ {"a", "b", "2.2-SNAPSHOT", Artifact.JAR }, {"c", "d", "2.2.4.6-SNAPSHOT", Artifact.JAR}, {"e", "f", "2-SNAPSHOT", Artifact.JAR} }; } @Test(dataProvider = "snapshotArtifacts") public void shouldRecognizeSnapshots( String groupId, String artifactId, String version, Type type) { Artifact artifact = new Artifact(groupId, artifactId, version, type); assertThat(artifact.isSnapshot()).isTrue(); }
  • 52. Only version matters @DataProvider public Object[][] snapshotVersions() { return new Object[][]{ {"2.2-SNAPSHOT"}, {"2.2.4.6-SNAPSHOT"}, {"2-SNAPSHOT"} }; } @Test(dataProvider = "snapshotVersions") public void shouldRecognizeSnapshots(String version) { Artifact artifact = new Artifact(VALID_GROUP, VALID_ARTIFACT_ID, version, VALID_TYPE); assertThat(artifact.isSnapshot()).isTrue(); }
  • 54. Test methods names are important • testFindTransactionsToAutoCharge() • testSystemSuccess() • testOperation()
  • 55. Test methods names are important • testFindTransactionsToAutoCharge() • testSystemSuccess() • testOperation() @Test public void testOperation() { configureRequest("/validate") rc = new RequestContext(parser, request) assert rc.getConnector() == null assert rc.getOperation().equals("validate") }
  • 56. “should” is better than “test” • testOperation() • testQuery() • testConstructor() • testFindUsersWithFilter() • shouldRejectInvalidRequests() • shouldSaveNewUserToDatabase() • constructorShouldFailWithNegativePrice() • shouldReturnOnlyUsersWithGivenName()
  • 57. “should” is better than “test” • Starting test method names with “should” steers you in the right direction. http://jochopra.blogspot.com/ • “test” prefix makes your test method a limitless bag where you throw everything worth testing http://www.greenerideal.com/
  • 58. Test methods names are important @Test public void testQuery(){ when(q.getResultList()).thenReturn(null); assertNull(dao.findByQuery(Transaction.class, q, false)); assertNull(dao.findByQuery(Operator.class, q, false)); assertNull(dao.findByQuery(null, null, false)); List result = new LinkedList(); when(q.getResultList()).thenReturn(result); assertEquals(dao.findByQuery(Transaction.class, q, false), result); assertEquals(dao.findByQuery(Operator.class, q, false), result); assertEquals(dao.findByQuery(null, null, false), null); when(q.getSingleResult()).thenReturn(null); assertEquals(dao.findByQuery(Transaction.class, q, true).size(), 0); assertEquals(dao.findByQuery(Operator.class, q, true).size(), 0); assertEquals(dao.findByQuery(null, null, true), null); when(q.getSingleResult()).thenReturn(t); assertSame(dao.findByQuery(Transaction.class, q, true).get(0), t); when(q.getSingleResult()).thenReturn(o); assertSame(dao.findByQuery(Operator.class, q, true).get(0), o); when(q.getSingleResult()).thenReturn(null); assertSame(dao.findByQuery(null, null, true), null); }
  • 60. Asserting using private methods @Test public void testChargeInRetryingState() throws Exception { // given TxDTO request = createTxDTO(RequestType.CHARGE); AndroidTransaction androidTransaction = ... // when final TxDTO txDTO = processor.processRequest(request); // then assertState(request, androidTransaction, CHARGED, CHARGE_PENDING, AS_ANDROID_TX_STATE, ClientMessage.SUCCESS, ResultCode.SUCCESS); }
  • 61. Matchers vs. private methods assertState(TxDTO txDTO, AndroidTransaction androidTransaction, AndroidTransactionState expectedAndroidState, AndroidTransactionState expectedPreviousAndroidState, ExtendedState expectedState, String expectedClientStatus, ResultCode expectedRequestResultCode) { final List<AndroidTransactionStep> steps = new ArrayList<>(androidTransaction.getTransactionSteps()); final boolean checkPreviousStep = expectedAndroidState != null; assertTrue(steps.size() >= (checkPreviousStep ? 3 : 2)); if (checkPreviousStep) { AndroidTransactionStep lastStep = steps.get(steps.size() - 2); assertEquals(lastStep.getTransactionState(), expectedPreviousAndroidState); } final AndroidTransactionStep lastStep = steps.get(steps.size() - 1); assertEquals(lastStep.getTransactionState(), expectedAndroidState); assertEquals(lastStep.getMessage(), expectedClientStatus); assertEquals(txDTO.getResultCode(), expectedRequestResultCode); assertEquals(androidTransaction.getState(), expectedAndroidState); assertEquals(androidTransaction.getExtendedState(), expectedState); if (expectedClientStatus == null) { verifyZeroInteractions(client); } }
  • 62. Matchers vs. private methods @Test public void testChargeInRetryingState() throws Exception { // given TxDTO request = createTxDTO(CHARGE); AndroidTransaction androidTransaction = ... // when final TxDTO txDTO = processor.processRequest(request); // then assertThat(androidTransaction).hasState(CHARGED) .hasMessage(ClientMessage.SUCCESS) .hasPreviousState(CHARGE_PENDING) .hasExtendedState(null); assertEquals(txDTO.getResultCode(), ResultCode.SUCCESS); }
  • 63. Assertion part is freaking huge public void shouldPreDeployApplication() { // given Artifact artifact = mock(Artifact.class); when(artifact.getFileName()).thenReturn("war-artifact-2.0.war"); ServerConfiguration config = new ServerConfiguration(ADDRESS, USER, KEY_FILE, TOMCAT_PATH, TEMP_PATH); Tomcat tomcat = new Tomcat(HTTP_TOMCAT_URL, config); String destDir = new File(".").getCanonicalPath() + SLASH + "target" + SLASH; new File(destDir).mkdirs(); // when tomcat.preDeploy(artifact, new FakeWar(WAR_FILE_LENGTH)); //then JSch jsch = new JSch(); jsch.addIdentity(KEY_FILE); Session session = jsch.getSession(USER, ADDRESS, 22); session.setConfig("StrictHostKeyChecking", "no"); session.connect(); Channel channel = session.openChannel("sftp"); session.setServerAliveInterval(92000); channel.connect(); ChannelSftp sftpChannel = (ChannelSftp) channel; sftpChannel.get(TEMP_PATH + SLASH + artifact.getFileName(), destDir); sftpChannel.exit(); session.disconnect(); File downloadedFile = new File(destDir, artifact.getFileName()); assertThat(downloadedFile).exists().hasSize(WAR_FILE_LENGTH); }
  • 64. Just say it public void shouldPreDeployApplication() { Artifact artifact = mock(Artifact.class); when(artifact.getFileName()) .thenReturn(ARTIFACT_FILE_NAME); ServerConfiguration config = new ServerConfiguration(ADDRESS, USER, KEY_FILE, TOMCAT_PATH, TEMP_PATH); Tomcat tomcat = new Tomcat(HTTP_TOMCAT_URL, config); tomcat.preDeploy(artifact, new FakeWar(WAR_FILE_LENGTH)); SSHServerAssert.assertThat(ARTIFACT_FILE_NAME) .existsOnServer(config).hasSize(WAR_FILE_LENGTH); }
  • 65. What is asserted? @Test public void testCompile_32Bit_FakeSourceFile() { CompilerSupport _32BitCompilerSupport = CompilerSupportFactory.getDefault32BitCompilerSupport(); testCompile_FakeSourceFile(_32BitCompilerSupport); }
  • 66. What is asserted? @Test public void testCompile_32Bit_FakeSourceFile() { CompilerSupport _32BitCompilerSupport = CompilerSupportFactory.getDefault32BitCompilerSupport(); testCompile_FakeSourceFile(_32BitCompilerSupport); } private void testCompile_FakeSourceFile( CompilerSupport compilerSupport) { compiledFiles = compilerSupport.compile(new File[] { new File("fake") }); assertThat(compiledFiles, is(emptyArray())); }
  • 67. Asserting everything public void invalidTxShouldBeCanceled() { String fileContent = FileUtils.getContentOfFile("response.csv"); assertTrue(fileContent.contains( "CANCEL,123,123cancel,billing_id_123_cancel,SUCCESS,")); }
  • 68. Asserting everything public void invalidTxShouldBeCanceled() { String fileContent = FileUtils.getContentOfFile("response.csv"); assertTrue(fileContent.contains( "CANCEL,123,123cancel,billing_id_123_cancel,SUCCESS,")); } public void invalidTxShouldBeCanceled() { String fileContent = FileUtils.getContentOfFile("response.csv"); TxDTOAssert.assertThat(fileContent) .hasTransaction("123cancel").withResultCode(SUCCESS); }
  • 70. Expected exceptions @Test(expectedExceptions = SmsException.class) public void shouldThrowException() throws SmsException { try { String s = gutExtractor.extractGut(„invalid gut”); System.out.println(s); } catch (SmsException e) { e.printStackTrace(); throw e; } }
  • 71. Expected exceptions @Test(expectedExceptions = SmsException.class) public void shouldThrowException() throws SmsException { String s = gutExtractor.extractGut(„invalid gut”); }
  • 72. Expected exceptions (with catch-exception) @Test public void shouldThrowException() throws SmsException { when(gutExtractor.extractGut(„invalid gut”)); then(caughtException()) .isInstanceOf(SmsException.class) .hasMessage("Invalid gut") .hasNoCause(); } http://code.google.com/p/catch-exception/
  • 73. Running SUT's code concurrently @Test(threadPoolSize = 3, invocationCount = 10) public void testServer() { // this method will be run in parallel by 3 thread // 10 invocations (in total) }
  • 74. Dependent test methods @Test public void shouldConnectToDB() { // verifying that you can // estabilish a connection with DB } @Test(dependsOnMethods = „shouldConnectToDB”) public void should…() { // some operations on DB }
  • 75. Know your tools • Unit testing framework  Additional libraries  Use of temporary file rule  Hamcrest, FEST, Mockito,  Listeners catch-exception, …  Concurrency • Build tool  @Before/@After  Parallel execution  Parametrized tests  CI  Test dependencies • IDE  Templates  Shortcuts
  • 76. What do you really want to test?
  • 77. What do you really want to test? @Test public void shouldAddAUser() { User user = new User(); userService.save(user); assertEquals(dao.getNbOfUsers(), 1); }
  • 78. You wanted to see that the number increased @Test public void shouldAddAUser() { Int nb = dao.getNbOfUsers(); User user = new User(); userService.save(user); assertEquals(dao.getNbOfUsers(), nb + 1); }
  • 80. Doing it wrong public void myTest() { SomeObject obj = new SomeObject( a, b, c, productCode()); // testing of obj here } private String productCode(){ String[] codes = {"Code A", "Code B", "Code C", "Code D"}; int index = rand.nextInt(codes.length); return codes[index]; }
  • 81. The dream of stronger, random-powered tests public void myTest() { SomeObject obj = new SomeObject( randomName(), randomValue(), ....); // testing of obj here } Does it make your test stronger?
  • 82. The dream of stronger, random-powered tests public void myTest() { SomeObject obj = new SomeObject( randomName(), randomValue(), ....); // testing of obj here } Does it make your test stronger? ...or does it only bring confusion? Test failed Expected SomeObject(„a”, „b”, ....) but got Expected SomeObject(„*&O*$NdlF”, „#idSLNF”, ....)
  • 84. There is more to it • Integration / end-to-end tests which are not parametrized (so they all try to set up jetty on port 8080), • Tests which should be really unit, but use Spring context to create objects, • Tests with a lot of dependencies between them (a nightmare to maintain!), • Tests which run slow • Tests which try to cover the deficiencies of production code and end up being a total mess, • etc., etc.
  • 85. Test-last? No! • makes people not write tests at all • makes people do only happy-testing • tests reflect the implementation
  • 86. Always TDD? For six or eight hours spread over the next few weeks I struggled to get the first test written and running. Writing tests for Eclipse plug-ins is not trivial, so it’s not surprising I had some trouble. [...] In six or eight hours of solid programming time, I can still make significant progress. If I’d just written some stuff and verified it by hand, I would probably have the final answer to whether my idea is actually worth money by now. Instead, all I have is a complicated test that doesn’t work, a pile of frustration, eight fewer hours in my life, and the motivation to write another essay. Ken Beck, Just Ship, Baby
  • 87. Treat tests as the first class citizens • do it everyday or forget about it • make tests readable using matchers, • use the right tool for the job builders and good names • and learn to use it! • test behaviour not methods • do not live with broken windows • be pragmatic about the tests you write • respect KISS, SRP, DRY (?) • TDD always? • write good code, and you will also write • what is the best way to test it? good tests unit/integration/end-to-end ? • or rather write good tests and you • automate! will get good code for free • always concentrate on what is worth • code review your tests testing • do more than happy path testing • ask yourself questions like: 'is it really important that X should send • do not make the reader learn the API, message Y to Z?' make it obvious • use the front door – state testing before • bad names lead to bad tests interaction testing (mockc)
  • 88. …questions? …rants? …hate speeches? …any other forms of expressing your ego? P.S. If you have some „interesting” tests, I would be happy to see them. Send them to me, please!
  • 89. Thank you! Thank you for watching these slides! You can learn more about wirting high quality tests by reading my book – „Practical Unit Testing with TestNG and Mockito”. Regards, Tomek Kaczanowski http://practicalunittesting.com