Bad Tests, Good Tests
Tomek Kaczanowski
http://twitter.com/#!/devops_borat
Tomek Kaczanowski
• Developer
• Team lead
• Blogger
• http://kaczanowscy.pl/tomek

• Book author
• http://practicalunittesting.com

• Trainer
• Interested? Contact me!
• kaczanowski.tomek@gmail.com

• Trainings in Vienna? Sure!
Before we begin
• All of the examples are real but were:
• obfuscated
• to protect the innocents :)
• truncated
• imagine much more complex domain objects
• Asking questions is allowed
• ...but being smarter than me is not ;)
Who writes tests?
What about the ROI?
http://ripper1331.deviantart.com
http://ripper1331.deviantart.com
http://ripper1331.deviantart.com

Survey is show
junior devops
are still believe
in Tooth Fairy,
Santa Claus and
documentation
http://twitter.com/#!/devops_borat
http://ripper1331.deviantart.com

Survey is show
junior devops
are still believe
in Tooth Fairy,
Santa Claus and
documentation
http://twitter.com/#!/devops_borat

If you think
good design
is expensive,
you should look
at the cost
of bad design.
Dr Ralph Speth, CEO Jaguar
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/
write the right test
write the right test

write this test right
Ancient times
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 ");
}

Courtesy of @bocytko
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();
}
}

Courtesy of @bocytko
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 null - 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();
}
}

Courtesy of @bocytko
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();
}
}

Courtesy of @bocytko
No smoke without tests
class SystemAdminSmokeTest extends GroovyTestCase {
void testSmoke() {
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
}
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);
}
}
Conclusions
• Automation!
• Running
• Manual verification is evil
• Do not live with broken window
• If you don't fix things no one else will!
• It is a full time job!
• You should be informed why your test failed
• Master your tools
• …at least learn the basics!
Mocks & Co.
@Test
public void shouldGetTrafficTrend() {
//given
TrafficTrendProvider trafficTrendProvider
= mock(TrafficTrendProvider.class);
Report report = new Report(null, "", 1, 2, 3,
BigDecimal.ONE, BigDecimal.ONE, 1);
TrafficTrend trafficTrend = new TrafficTrend(report, report,
new Date(), new Date(), new Date(), new Date());
given(trafficTrendProvider.getTrafficTrend())
.willReturn(trafficTrend);
TrafficService service
= new TrafficService(trafficTrendProvider);
//when
TrafficTrend result = service.getTrafficTrend();
//then
assertThat(result).isEqualTo(trafficTrend);
}
Use of the real objects obscures the test
@Test
public void shouldGetTrafficTrend() {
//given
TrafficTrendProvider trafficTrendProvider
= mock(TrafficTrendProvider.class);
TrafficTrend trafficTrend = mock(TrafficTrend.class);
given(trafficTrendProvider.getTrafficTrend())
.willReturn(trafficTrend);

TrafficService service
= new TrafficService(trafficTrendProvider);
//when
TrafficTrend result = service.getTrafficTrend();
//then
assertThat(result).isEqualTo(trafficTrend);
}
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");
}
Mock'em All!

ModelAndView from
SpringMVC – a mere container
for data, without any behaviour

@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");
}
Do not test interactions if not needed
@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

a pseudocode but that is
what we mean

assertThat(modelAndView).contains("timezone", "timezone X");
}
public class Util {
public String getUrl(User user, String timestamp) {
String name = user.getFullName();
String url = baseUrl
+"name="+URLEncoder.encode(name, "UTF-8")
+"&timestamp="+timestamp;
Developer wants to check
return url;
whether timestamp is added
}

to
the URL when this method is
used

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);
}
}
public class Util {

Developer
public String getUrl(User user, String timestamp) { wants to check
...
whether timestamp is added to
}
the URL when this method is
public String getUrl(User user) {
...
}
}

used
Bad design → bad tests
public class Util {

Developer
public String getUrl(User user, String timestamp) { wants to check
...
whether timestamp is added to
}
the URL when this method is
public String getUrl(User user) {
...
}

used

}
@Test
public void shouldUseTimestampMethod() {
//given
Util util = new Util();
Util spyUtil = Mockito.spy(util);
//when
spyUtil.getUrl(user);
//then
verify(spyUtil).getUrl(eq(user), anyString());
}
Dependency Injection will save us
@Test
public void shouldGenerateURLWithTimestamp() {
//given
TimeProvider timeProvider = mock(TimeProvider.class);
Util util = new Util(timeProvider);
given(timeProvider.getTime()).willReturn("12345");
util.set(timeProvider);
//when
String url = util.getUrl(user);
//then
assertThat(url).contains("timestamp=12345");
}
Single Responsibility Principle

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},
{"++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,
}
testQueryVerification2() {
assertEquals(true,
}
testQueryVerification3() {
assertEquals(true,
}
testQueryVerification4() {
assertEquals(true,
}
...

FieldVerifier.isValidQuery(„48”));
FieldVerifier.isValidQuery(„+48”));
FieldVerifier.isValidQuery(„++48”));
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));
}
“And”
@Test
public void shouldReturnRedirectViewAndSendEmail() {
//given
given(bindingResult.hasErrors()).willReturn(false);
given(userData.toEntity()).willReturn(user);
given(userService.saveNewUser(eq(userData.toEntity())))
.willReturn(user);
//when
ModelAndView userRegisterResult = userRegisterController
.registerUser(userData, bindingResult, request);
//then
assertThat(userRegisterResult.getViewName())
.isEqualTo("redirect:/signin");
verify(mailSender).sendRegistrationInfo(user);
}
One feature at a time
@Test
public void shouldRedirectToSigninPageWhenRegistrationSuceeded () {
...
}
@Test
public void shouldNotifyAboutNewUserRegistration() {
...
}

Hint: forget about methods
Readability is the king
variables
@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}
};
}
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}
};
}

Who the heck is
“user_2”?!
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}
};
}
domain1, domain2, domain3, ...
domain1, domain2, domain3, ...
domain1, domain2, domain3, ...
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 = createFileNonSSLMockServer(responseMap);
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();
server = MockServerBuilder
.createFileNoSSLServer(responseMap, SERVER_ROOT);
Readability is the king
test method names
Test methods names are important
• When test fails
• Relation to focused tests
What is it all about?

@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);
Test methods names are important
@Test
public void shouldReturnNullListWhenDaoReturnsNull {
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));
}
public void shouldReturnEmptyListWhenDaoReturnsIt {
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);
}
public void shouldReturnNullSingleResultWhenDaoReturnsNull {
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);
}
public void shouldReturnSingleResultReturnedByDao {
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
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() {
// given
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);
// when
tomcat.preDeploy(artifact, new FakeWar(WAR_FILE_LENGTH));
// then
SSHServerAssert.assertThat(ARTIFACT_FILE_NAME)
.existsOnServer(tomcat).hasSize(WAR_FILE_LENGTH);
}
Just say it
public void shouldPreDeployApplication() {
// given
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);

WHY NOT CREATE
A PRIVATE ASSERTION METHOD?

// when
WHY NOT USE
tomcat.preDeploy(artifact, new FakeWar(WAR_FILE_LENGTH));
// then
assertThatFileIsOnServer(ARTIFACT_FILE_NAME,
Tomcat, WAR_FILE_LENGTH);
}
Assertions repeated in many tests
@Test
public void testChargeInRetryingState() throws Exception {
// given
TxDTO request = createTxDTO(RequestType.CHARGE);
AndroidTransaction androidTransaction = ...
request.setTransaction(androidTransaction);
// when
final TxDTO txDTO = processor.processRequest(request);
// then
List<AndroidTransactionStep> steps
= new ArrayList<>(androidTransaction.getSteps());
AndroidTransactionStep lastStep = steps.get(steps.size() - 1);
assertEquals(lastStep.getTransactionState(), CHARGED_PENDING);
assertEquals(txDTO.getResultCode(), CHARGED);
}
Assertions repeated in many tests
@Test
public void testChargeInRetryingState() throws Exception {
// given
TxDTO request = createTxDTO(RequestType.CHARGE);
AndroidTransaction androidTransaction = ...
request.setTransaction(androidTransaction);

WHY NOT CREATE
A PRIVATE ASSERTION METHOD?
// when
final TxDTO txDTO = processor.processRequest(request);

// then
List<AndroidTransactionStep> steps
= new ArrayList<>(androidTransaction.getSteps());
AndroidTransactionStep lastStep = steps.get(steps.size() - 1);
assertEquals(lastStep.getTransactionState(), CHARGED_PENDING);
assertEquals(txDTO.getResultCode(), CHARGED);
}
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);
}
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);
}
}
Custom assertions to the rescue
@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);
}
Asserting implementation details
public void invalidTxShouldBeCanceled() {
...
String fileContent =
FileUtils.getContentOfFile("response.csv");
assertTrue(fileContent.contains(
"CANCEL,123,123cancel,billing_id_123_cancel,SUCCESS,"));
}
Asserting implementation details
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 tools
• Unit testing framework
• Use of temporary file rule

• Additional libraries
• Hamcrest, FEST, Mockito,
catch-exception, awaitility,
JUnitParams, tempus-fugit, …

• Listeners
• Concurrency
• @Before/@After
• Parametrized tests
• Test dependencies

• Build tool
• Parallel execution
• CI
• IDE
• Templates
• Shortcuts
Expected exceptions
@Test(expected=IndexOutOfBoundsException.class)
public void shouldThrowExceptionGettingElementOutsideTheList() {
MyList<Integer> list = new MyList<Integer>();
list.add(0);
list.add(1);
list.get(2);
}
Expected exceptions
@Test(expected=IndexOutOfBoundsException.class)
public void shouldThrowExceptionGettingElementOutsideTheList() {
MyList<Integer> list = new MyList<Integer>();
list.add(0);
list.add(1);
list.get(2);
}
http://code.google.com/p/catch-exception/

@Test

public void shouldThrowExceptionGettingtElementOutsideTheList() {
MyList<Integer> list = new MyList<Integer>();
list.add(0);
list.add(1);
catchException(list).get(2);
assertThat(caughtException())

.isExactlyInstanceOf(IndexOutOfBoundsException.cla
}
Awaitility
@Test
public void updatesCustomerStatus() throws Exception {
// Publish an asynchronous event:
publishEvent(updateCustomerStatusEvent);

}

// Awaitility lets you wait until
// the asynchronous operation completes:
await().atMost(5, SECONDS)
.until(costumerStatusIsUpdated());
...
http://code.google.com/p/awaitility/
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);
}
Because:
1) This is closer to what you wanted to test
2) There is no assumption about the database “users” table being empty
Asking for troubles...
LoggingPropertyConfigurator configurator = mock(...);
BaseServletContextListener baseServletContextListener =
= new BaseServletContextListener(configurator)
@Test public void shouldLoadConfigProperties() {

Should load some
default config
baseServletContextListener.contextInitialized();
verify(configurator).configure(any(Properties.class));

}

Should load this
specific file

@Test(expected = LoggingInitialisationException.class)
public void shouldThrowExceptionIfCantLoadConfiguration() {
System.setProperty("logConfig", "nonExistingFile");
baseServletContextListener.contextInitialized();
}
Asking for troubles...
LoggingPropertyConfigurator configurator = mock(...);
BaseServletContextListener baseServletContextListener =
= new BaseServletContextListener(configurator)
@Test public void shouldLoadConfigProperties() {
baseServletContextListener.contextInitialized();
verify(configurator).configure(any(Properties.class));
}
@Test(expected = LoggingInitialisationException.class)
public void shouldThrowExceptionIfCantLoadConfiguration() {
System.setProperty("logConfig", "nonExistingFile");
baseServletContextListener.contextInitialized();
}
@Before
public void cleanSystemProperties() {
...
}
Ceremony
@Test
public void shouldBeAdministrator() {
//given
User user = new Administrator();
//when
boolean administrator = user.isAdministrator();
boolean advertiser = user.isAdvertiser();
boolean domainer = user.isDomainer();
//then
assertThat(administrator).isTrue();
assertThat(advertiser).isFalse();
assertThat(domainer).isFalse();
}
Ceremony
@Test
public void shouldBeAdministrator() {
User user = new Administrator();
assertThat(user.isAdministrator()).isTrue();
assertThat(user.isAdvertiser()).isFalse();
assertThat(user.isDomainer()).isFalse();
}
private static final int PER_PAGE = 10;
@Test
public void shouldGiveOffsetZeroWhenOnZeroPage() {
Pager pager = new Pager(PER_PAGE);
assertThat(pager.getOffset()).isEqualTo(0);
}
@Test
public void shouldIncreaseOffsetWhenGoingToPageOne() {
Pager pager = new Pager(PER_PAGE);
pager.goToNextPage();
assertThat(pager.getOffset()).isEqualTo(PER_PAGE);
}
private static final int PER_PAGE = 10;
@Test
public void shouldGiveOffsetZeroWhenOnZeroPage() {
Pager pager = new Pager(PER_PAGE);
assertThat(pager.getOffset()).isEqualTo(0);
}
@Test
public void shouldIncreaseOffsetWhenGoingToPageOne() {
Pager pager = new Pager(PER_PAGE);
pager.goToNextPage();
assertThat(pager.getOffset()).isEqualTo(PER_PAGE);
}

public void goToNextPage() {
this.offset = +perPage;
}
Test-last?

• makes people not write tests at all
• makes people do only happy path testing
• tests reflect the implementation
Treat tests as the first class citizens
•

do it everyday or forget about it

•

•

use the right tool for the job
• and learn to use it!

make tests readable using matchers,
builders and good names

•

test behaviour not methods

•

do not live with broken windows

•

•

respect KISS, SRP, DRY (?)

be pragmatic about the tests you write
• TDD always?

•

write good code, and you will also write
good tests
• or rather write good tests and you
will get good code for free

•

do not make the reader learn the API, make
it obvious

•

bad names lead to bad tests

automate!

•

always concentrate on what is worth testing
• ask yourself questions like: 'is it
really important that X should send
message Y to Z?'

•

use the front door – state testing before
interaction testing (mocks)

do more than happy path testing

•

what is the best way to test it?
unit/integration/end-to-end ?

•

code review your tests

•

•
Thank you!
You can learn more about writing
high quality tests by reading my
book – „Practical Unit Testing”.
You can also participate in writing
of my new (free!) e-book devoted to
bad and good tests.

2013 DevFest Vienna - Bad Tests, Good Tests

  • 1.
    Bad Tests, GoodTests Tomek Kaczanowski
  • 2.
  • 3.
    Tomek Kaczanowski • Developer •Team lead • Blogger • http://kaczanowscy.pl/tomek • Book author • http://practicalunittesting.com • Trainer • Interested? Contact me! • kaczanowski.tomek@gmail.com • Trainings in Vienna? Sure!
  • 4.
    Before we begin •All of the examples are real but were: • obfuscated • to protect the innocents :) • truncated • imagine much more complex domain objects • Asking questions is allowed • ...but being smarter than me is not ;)
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
    http://ripper1331.deviantart.com Survey is show juniordevops are still believe in Tooth Fairy, Santa Claus and documentation http://twitter.com/#!/devops_borat
  • 10.
    http://ripper1331.deviantart.com Survey is show juniordevops are still believe in Tooth Fairy, Santa Claus and documentation http://twitter.com/#!/devops_borat If you think good design is expensive, you should look at the cost of bad design. Dr Ralph Speth, CEO Jaguar
  • 11.
    Tests help toachieve quality Not sure when I saw this picture – probably in GOOS?
  • 12.
    What happens ifwe do it wrong? • Angry clients • Depressed developers http://www.joshcanhelp.com
  • 13.
    When I startedout 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
  • 14.
  • 15.
  • 16.
    write the righttest write this test right
  • 17.
  • 18.
    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 "); } Courtesy of @bocytko
  • 19.
    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(); } } Courtesy of @bocytko
  • 20.
    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 null - 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(); } } Courtesy of @bocytko
  • 21.
    Success is notan option... /** * Method testFailure. */ public void testFailure() { try { Message message = new Message(null,true); fail(); } catch(Exception ex) { ExceptionHandler.log(ExceptionLevel.ANY,ex); fail(); } } Courtesy of @bocytko
  • 22.
    No smoke withouttests class SystemAdminSmokeTest extends GroovyTestCase { void testSmoke() { 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 }
  • 23.
    No smoke withouttests 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 :( }
  • 24.
    Let's follow theleader! @Test public class ExampleTest { public void testExample() { assertTrue(true); } }
  • 25.
    Uh-oh, I feellonely... @Test public class ExampleTest { public void testExample() { assertTrue(true); } }
  • 26.
    Conclusions • Automation! • Running •Manual verification is evil • Do not live with broken window • If you don't fix things no one else will! • It is a full time job! • You should be informed why your test failed • Master your tools • …at least learn the basics!
  • 27.
  • 28.
    @Test public void shouldGetTrafficTrend(){ //given TrafficTrendProvider trafficTrendProvider = mock(TrafficTrendProvider.class); Report report = new Report(null, "", 1, 2, 3, BigDecimal.ONE, BigDecimal.ONE, 1); TrafficTrend trafficTrend = new TrafficTrend(report, report, new Date(), new Date(), new Date(), new Date()); given(trafficTrendProvider.getTrafficTrend()) .willReturn(trafficTrend); TrafficService service = new TrafficService(trafficTrendProvider); //when TrafficTrend result = service.getTrafficTrend(); //then assertThat(result).isEqualTo(trafficTrend); }
  • 29.
    Use of thereal objects obscures the test @Test public void shouldGetTrafficTrend() { //given TrafficTrendProvider trafficTrendProvider = mock(TrafficTrendProvider.class); TrafficTrend trafficTrend = mock(TrafficTrend.class); given(trafficTrendProvider.getTrafficTrend()) .willReturn(trafficTrend); TrafficService service = new TrafficService(trafficTrendProvider); //when TrafficTrend result = service.getTrafficTrend(); //then assertThat(result).isEqualTo(trafficTrend); }
  • 30.
    Mock'em All! @Test public voidshouldAddTimeZoneToModelAndView() { //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.
    Mock'em All! ModelAndView from SpringMVC– a mere container for data, without any behaviour @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"); }
  • 32.
    Do not testinteractions if not needed @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 a pseudocode but that is what we mean assertThat(modelAndView).contains("timezone", "timezone X"); }
  • 33.
    public class Util{ public String getUrl(User user, String timestamp) { String name = user.getFullName(); String url = baseUrl +"name="+URLEncoder.encode(name, "UTF-8") +"&timestamp="+timestamp; Developer wants to check return url; whether timestamp is added } to the URL when this method is used 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); } }
  • 34.
    public class Util{ Developer public String getUrl(User user, String timestamp) { wants to check ... whether timestamp is added to } the URL when this method is public String getUrl(User user) { ... } } used
  • 35.
    Bad design →bad tests public class Util { Developer public String getUrl(User user, String timestamp) { wants to check ... whether timestamp is added to } the URL when this method is public String getUrl(User user) { ... } used } @Test public void shouldUseTimestampMethod() { //given Util util = new Util(); Util spyUtil = Mockito.spy(util); //when spyUtil.getUrl(user); //then verify(spyUtil).getUrl(eq(user), anyString()); }
  • 36.
    Dependency Injection willsave us @Test public void shouldGenerateURLWithTimestamp() { //given TimeProvider timeProvider = mock(TimeProvider.class); Util util = new Util(timeProvider); given(timeProvider.getTime()).willReturn("12345"); util.set(timeProvider); //when String url = util.getUrl(user); //then assertThat(url).contains("timestamp=12345"); }
  • 37.
    Single Responsibility Principle Atest should have one and only one reason to fail.
  • 38.
    Testing two thingsat 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)); }
  • 39.
    Testing two thingsat 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, } testQueryVerification2() { assertEquals(true, } testQueryVerification3() { assertEquals(true, } testQueryVerification4() { assertEquals(true, } ... FieldVerifier.isValidQuery(„48”)); FieldVerifier.isValidQuery(„+48”)); FieldVerifier.isValidQuery(„++48”)); FieldVerifier.isValidQuery(„+48503”));
  • 40.
    Testing two thingsat 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)); }
  • 41.
    Concentrate on onefeature @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)); }
  • 42.
    “And” @Test public void shouldReturnRedirectViewAndSendEmail(){ //given given(bindingResult.hasErrors()).willReturn(false); given(userData.toEntity()).willReturn(user); given(userService.saveNewUser(eq(userData.toEntity()))) .willReturn(user); //when ModelAndView userRegisterResult = userRegisterController .registerUser(userData, bindingResult, request); //then assertThat(userRegisterResult.getViewName()) .isEqualTo("redirect:/signin"); verify(mailSender).sendRegistrationInfo(user); }
  • 43.
    One feature ata time @Test public void shouldRedirectToSigninPageWhenRegistrationSuceeded () { ... } @Test public void shouldNotifyAboutNewUserRegistration() { ... } Hint: forget about methods
  • 44.
    Readability is theking variables
  • 45.
    @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.
    Who the heckis “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} }; } Who the heck is “user_2”?!
  • 47.
    Ah, logged usercan 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} }; }
  • 48.
  • 49.
  • 50.
  • 51.
    Do not makeme learn the API! server = new MockServer(responseMap, true, new URL(SERVER_ROOT).getPort(), false);
  • 52.
    Do not makeme 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);
  • 53.
    Do not makeme learn the API! server = new MockServer(responseMap, true, new URL(SERVER_ROOT).getPort(), false); server = createFileNonSSLMockServer(responseMap);
  • 54.
    Do not makeme 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(); server = MockServerBuilder .createFileNoSSLServer(responseMap, SERVER_ROOT);
  • 55.
    Readability is theking test method names
  • 56.
    Test methods namesare important • When test fails • Relation to focused tests
  • 58.
    What is itall about? @Test public void testOperation() { configureRequest("/validate") rc = new RequestContext(parser, request) assert rc.getConnector() == null assert rc.getOperation().equals("validate") }
  • 59.
    “should” is betterthan “test” • • • • testOperation() testQuery() testConstructor() testFindUsersWithFilter() • • • • shouldRejectInvalidRequests() shouldSaveNewUserToDatabase() constructorShouldFailWithNegativePrice() shouldReturnOnlyUsersWithGivenName()
  • 60.
    “should” is betterthan “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/
  • 61.
    Test methods namesare 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);
  • 62.
    Test methods namesare important @Test public void shouldReturnNullListWhenDaoReturnsNull { 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)); } public void shouldReturnEmptyListWhenDaoReturnsIt { 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); } public void shouldReturnNullSingleResultWhenDaoReturnsNull { 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); } public void shouldReturnSingleResultReturnedByDao { 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); }
  • 63.
  • 64.
    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);
  • 65.
    Just say it publicvoid shouldPreDeployApplication() { // given 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); // when tomcat.preDeploy(artifact, new FakeWar(WAR_FILE_LENGTH)); // then SSHServerAssert.assertThat(ARTIFACT_FILE_NAME) .existsOnServer(tomcat).hasSize(WAR_FILE_LENGTH); }
  • 66.
    Just say it publicvoid shouldPreDeployApplication() { // given 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); WHY NOT CREATE A PRIVATE ASSERTION METHOD? // when WHY NOT USE tomcat.preDeploy(artifact, new FakeWar(WAR_FILE_LENGTH)); // then assertThatFileIsOnServer(ARTIFACT_FILE_NAME, Tomcat, WAR_FILE_LENGTH); }
  • 67.
    Assertions repeated inmany tests @Test public void testChargeInRetryingState() throws Exception { // given TxDTO request = createTxDTO(RequestType.CHARGE); AndroidTransaction androidTransaction = ... request.setTransaction(androidTransaction); // when final TxDTO txDTO = processor.processRequest(request); // then List<AndroidTransactionStep> steps = new ArrayList<>(androidTransaction.getSteps()); AndroidTransactionStep lastStep = steps.get(steps.size() - 1); assertEquals(lastStep.getTransactionState(), CHARGED_PENDING); assertEquals(txDTO.getResultCode(), CHARGED); }
  • 68.
    Assertions repeated inmany tests @Test public void testChargeInRetryingState() throws Exception { // given TxDTO request = createTxDTO(RequestType.CHARGE); AndroidTransaction androidTransaction = ... request.setTransaction(androidTransaction); WHY NOT CREATE A PRIVATE ASSERTION METHOD? // when final TxDTO txDTO = processor.processRequest(request); // then List<AndroidTransactionStep> steps = new ArrayList<>(androidTransaction.getSteps()); AndroidTransactionStep lastStep = steps.get(steps.size() - 1); assertEquals(lastStep.getTransactionState(), CHARGED_PENDING); assertEquals(txDTO.getResultCode(), CHARGED); }
  • 69.
    Asserting using privatemethods @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); }
  • 70.
    assertState(TxDTO txDTO, AndroidTransactionandroidTransaction, 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); } }
  • 71.
    Custom assertions tothe rescue @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); }
  • 72.
    Asserting implementation details publicvoid invalidTxShouldBeCanceled() { ... String fileContent = FileUtils.getContentOfFile("response.csv"); assertTrue(fileContent.contains( "CANCEL,123,123cancel,billing_id_123_cancel,SUCCESS,")); }
  • 73.
    Asserting implementation details publicvoid 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); }
  • 74.
    Know your tools •Unit testing framework • Use of temporary file rule • Additional libraries • Hamcrest, FEST, Mockito, catch-exception, awaitility, JUnitParams, tempus-fugit, … • Listeners • Concurrency • @Before/@After • Parametrized tests • Test dependencies • Build tool • Parallel execution • CI • IDE • Templates • Shortcuts
  • 75.
    Expected exceptions @Test(expected=IndexOutOfBoundsException.class) public voidshouldThrowExceptionGettingElementOutsideTheList() { MyList<Integer> list = new MyList<Integer>(); list.add(0); list.add(1); list.get(2); }
  • 76.
    Expected exceptions @Test(expected=IndexOutOfBoundsException.class) public voidshouldThrowExceptionGettingElementOutsideTheList() { MyList<Integer> list = new MyList<Integer>(); list.add(0); list.add(1); list.get(2); } http://code.google.com/p/catch-exception/ @Test public void shouldThrowExceptionGettingtElementOutsideTheList() { MyList<Integer> list = new MyList<Integer>(); list.add(0); list.add(1); catchException(list).get(2); assertThat(caughtException()) .isExactlyInstanceOf(IndexOutOfBoundsException.cla }
  • 77.
    Awaitility @Test public void updatesCustomerStatus()throws Exception { // Publish an asynchronous event: publishEvent(updateCustomerStatusEvent); } // Awaitility lets you wait until // the asynchronous operation completes: await().atMost(5, SECONDS) .until(costumerStatusIsUpdated()); ... http://code.google.com/p/awaitility/
  • 78.
    What do youreally want to test? @Test public void shouldAddAUser() { User user = new User(); userService.save(user); assertEquals(dao.getNbOfUsers(), 1); }
  • 79.
    You wanted tosee that the number increased @Test public void shouldAddAUser() { int nb = dao.getNbOfUsers(); User user = new User(); userService.save(user); assertEquals(dao.getNbOfUsers(), nb + 1); } Because: 1) This is closer to what you wanted to test 2) There is no assumption about the database “users” table being empty
  • 80.
    Asking for troubles... LoggingPropertyConfiguratorconfigurator = mock(...); BaseServletContextListener baseServletContextListener = = new BaseServletContextListener(configurator) @Test public void shouldLoadConfigProperties() { Should load some default config baseServletContextListener.contextInitialized(); verify(configurator).configure(any(Properties.class)); } Should load this specific file @Test(expected = LoggingInitialisationException.class) public void shouldThrowExceptionIfCantLoadConfiguration() { System.setProperty("logConfig", "nonExistingFile"); baseServletContextListener.contextInitialized(); }
  • 81.
    Asking for troubles... LoggingPropertyConfiguratorconfigurator = mock(...); BaseServletContextListener baseServletContextListener = = new BaseServletContextListener(configurator) @Test public void shouldLoadConfigProperties() { baseServletContextListener.contextInitialized(); verify(configurator).configure(any(Properties.class)); } @Test(expected = LoggingInitialisationException.class) public void shouldThrowExceptionIfCantLoadConfiguration() { System.setProperty("logConfig", "nonExistingFile"); baseServletContextListener.contextInitialized(); } @Before public void cleanSystemProperties() { ... }
  • 82.
    Ceremony @Test public void shouldBeAdministrator(){ //given User user = new Administrator(); //when boolean administrator = user.isAdministrator(); boolean advertiser = user.isAdvertiser(); boolean domainer = user.isDomainer(); //then assertThat(administrator).isTrue(); assertThat(advertiser).isFalse(); assertThat(domainer).isFalse(); }
  • 83.
    Ceremony @Test public void shouldBeAdministrator(){ User user = new Administrator(); assertThat(user.isAdministrator()).isTrue(); assertThat(user.isAdvertiser()).isFalse(); assertThat(user.isDomainer()).isFalse(); }
  • 84.
    private static finalint PER_PAGE = 10; @Test public void shouldGiveOffsetZeroWhenOnZeroPage() { Pager pager = new Pager(PER_PAGE); assertThat(pager.getOffset()).isEqualTo(0); } @Test public void shouldIncreaseOffsetWhenGoingToPageOne() { Pager pager = new Pager(PER_PAGE); pager.goToNextPage(); assertThat(pager.getOffset()).isEqualTo(PER_PAGE); }
  • 85.
    private static finalint PER_PAGE = 10; @Test public void shouldGiveOffsetZeroWhenOnZeroPage() { Pager pager = new Pager(PER_PAGE); assertThat(pager.getOffset()).isEqualTo(0); } @Test public void shouldIncreaseOffsetWhenGoingToPageOne() { Pager pager = new Pager(PER_PAGE); pager.goToNextPage(); assertThat(pager.getOffset()).isEqualTo(PER_PAGE); } public void goToNextPage() { this.offset = +perPage; }
  • 86.
    Test-last? • makes peoplenot write tests at all • makes people do only happy path testing • tests reflect the implementation
  • 87.
    Treat tests asthe first class citizens • do it everyday or forget about it • • use the right tool for the job • and learn to use it! make tests readable using matchers, builders and good names • test behaviour not methods • do not live with broken windows • • respect KISS, SRP, DRY (?) be pragmatic about the tests you write • TDD always? • write good code, and you will also write good tests • or rather write good tests and you will get good code for free • do not make the reader learn the API, make it obvious • bad names lead to bad tests automate! • always concentrate on what is worth testing • ask yourself questions like: 'is it really important that X should send message Y to Z?' • use the front door – state testing before interaction testing (mocks) do more than happy path testing • what is the best way to test it? unit/integration/end-to-end ? • code review your tests • •
  • 88.
    Thank you! You canlearn more about writing high quality tests by reading my book – „Practical Unit Testing”. You can also participate in writing of my new (free!) e-book devoted to bad and good tests.