How to survive your unit tests

Post on 21-Apr-2017

782 views 7 download

Transcript of How to survive your unit tests

How to survive your unit tests@racingDeveloper - Gabriele Tondi

Gabriele Tondi @racingDeveloper

Agile Software Developer

Coordinatore: XPUG-MI

Appassionato di:

sviluppo software, metodologie agili (eXtreme Programming), OOD, TDD e motori

Story time

Greenfield Project

Test Driven Development

First Iteration

Product Owner Status

Developers Status

Nuova Funzionalità

Embrace Change

Cambiamento nel Design

Un piccolo cambiamento, tanti test rotti

Come sopravvivere?

Perché facciamo TDD

• Il codice che abbiamo scritto fa quello che ci aspettiamo?

• feedback immediato sul design

• una suite di test per poter fare refactoring spesso senza paura

Perché i nostri test si oppongono al refactoring?

Spesso:

• Non si riesce a capire perché il test fallisce

• Non si riesce a capire cosa vuole verificare il test

• Il test è fragile

• Il test è troppo accoppiato al codice di produzione

ATTENZIONE: il più delle volte un test è difficile da scrivere a causa di un problema

nel design del codice di produzione.

!

Focus di oggi è sul design dei test realizzati durante le iterazioni TDD

Semplici linee guida. L’insieme fa la differenza.

Un buon test

• è conciso e semplice

• quando fallisce riusciamo subito a capire dove si trova il problema

• rappresenta al meglio un valido esempio di comportamento

Naming

@Test public void testThatValuesLowerThan3AreInvalid() { assertFalse(validator.validate(2)); }

@Test public void testThatValuesLowerThan3AreInvalid() { assertFalse(validator.validate(2)); }

Evitare rumore nel nome dei test

Evitare duplicazione nel nome dei testLasciamo che sia il test stesso a dirci cosa succede

Suggerimento: identificare scenari con i nomi dei testEsempi: tooLowValue, notANumber

@Test

public void tooLowValue() { assertFalse(validator.validate(2)); }

Assert

@Test public void formattedErrorMessage() { assertTrue(formatter.format(NOT_FOUND_ERROR) .equals(“Book not found")); }

java.lang.AssertionError at org.junit.Assert.fail(Assert.java:86) at org.junit.Assert.assertTrue(Assert.java:41) at org.junit.Assert.assertTrue(Assert.java:52)

WTF ?!?!?

@Test public void formattedErrorMessage() { assertEquals(“Book not found”, formatter.format(NOT_FOUND_ERROR)); }

org.junit.ComparisonFailure: Expected :Book not found Actual :Libro non trovato <Click to see difference>

Guardate il test fallire!… sempre! Anche se per qualche motivo già passa.

Suggerimenti

• Usate asserzioni specifiche

• Se non fosse possibile, includete un messaggio

• assertFalse(“value is invalid”, validator.validate(…))

• Fate particolare attenzione ai custom matcher! (Hamcrest)

Act

@Test public void filledCart() { ShoppingCart cart = new ShoppingCart();

cart.add(new Item(“item 1”, 2)); assertEquals(2, cart.total());

cart.remove(“item 1”); assertEquals(0, cart.total());

}

Rischi

• Il test può fallire per due o più comportamenti diversi

• Cosa vogliamo veramente testare?

Suggerimenti• Una sola azione per test

• Evitare act -> assert, act -> assert …

• Sfruttare questo smell per spingere il design

• Nel caso specifico: perché non permettere la creazione di un carrello in uno stato preciso?

Arrange

ATTENZIONE: se il test richiede un setup complesso c’è quasi certamente un problema

con il design del codice di produzione.

!

@Test(expected = MissingTitleException.class) public void missingTitle() { new Book(1, “123485”, null, “an adventure book”); }

@Test(expected = MissingISBNException.class) public void missingISBN() { new Book(1, null, “Survive your unit tests", “an adventure book”); }

@Test(expected = MissingIDException.class) public void missingID() // […]

Rischi

• Quali dati sono importanti per ogni test?

• Se domani il codice ISBN deve essere di almeno 14 caratteri, quanti test devo cambiare?

Suggerimento

• Evitate dettagli inutili per il test

• Rendono più complicato capire cosa è importante per il test

• Rischiano di creare accoppiamento

• Potete evidenziare meglio problemi con il design

Object Mother• Classe con delle fixture pre-impostate

• Esempi:

• BookFixture.newBookWithMissingTitle ( )

• BookFixture.newBookWithMissingISBN ( )

• BookFixture.newBookWithMissingID ( )

Rischio

• Le fixture possono aumentare in maniera poco controllabile

• Questa soluzione potrebbe non scalare abbastanza

Builder

• Una classe che permette di generare un oggetto con uno stato preciso, partendo da valori di default sensibili

public class BookBuilder { private long id = 1; private String title = “A BOOK TITLE”; private String isbn = “123466579490”; private String abstract = “a test book”;

private BookBuilder() {}

public static BookBuilder aBook() { return new BookBuilder();

}

public BookBuilder withTitle(String title) { this.title = title; return this;

}

public Book build() { return new Book(id, title, isbn, abstract);

} }

@Test(expected = MissingTitleException.class) public void missingTitle() { aBook().withTitle(null).build(); }

@Test(expected = MissingISBNException.class) public void missingISBN() { aBook().withISBN(null).build(); }

Vantaggi• Ridotta duplicazione

• i valori di default sono impostati in un unico punto

• Aumentato l’espressività del test

• è molto chiara la correlazione tra input ed output, non dobbiamo specificare valori inutili per il test

Metodi privati nella classe di test

@Test public void obsoleteFlight() { givenAnObsoleteFlight(); whenITryToBookIt(); thenIGetAnError()

}

Vantaggi• l’espressività del comportamento è massima

Rischi• fatico ad ottenere feedback dal design

• posso nascondere di tutto nei metodi privati

• i nomi dei metodi potrebbero mentire!

Suggerimento• usate i metodi privati nei test con attenzione

• ogni metodo privato deve essere di 1 o 2 righe

• lo scopo è quello di generare un Domain Specific Language per il test

• un buon test dovrebbe essere leggibile anche senza metodi privati

Stato globale

@Test public void creationDate() { Date currentDate = new Date(); Book book = new Book();

assertThat(book.creationDate(), greaterThan(currentDate)); }

Suggerimenti

• Evitare il più possibile gli stati globali (ad esempio il tempo)

• È possibile iniettare un collaboratore con ruolo Clock (che può avere una implementazione programmabile)

• Oppure… possiamo passare direttamente il dato che ci interessa!

E quando abbiamo diversi messaggi tra diversi oggetti?

Incoming | Outgoing

Object Under TestIncoming

Outgoing

Command Query Separation• Query:

• un messaggio che torna qualcosa

• non ha side-effect

• Comando

• non torna nulla (void)

• ha dei side-effect

Test Object Under Test

Incoming Query Message

query message

Inviamo un messaggio dal test al oggetto sotto test

response

Oggetto sotto test elabora ed invia una risposta

Verifichiamo la risposta

Test Object Under Test

Incoming Command Message

command message

Inviamo un messaggio dal test al oggetto sotto test

Oggetto sotto test elabora e cambia lo stato

Verifichiamo gli effetti pubblici diretti

simple query on state

@Test public void soldBook() { Book book = new Book(); book.sell();

assertFalse(“book cannot be sold again”, book.canBeSold()); }

Esempio

Test Object Under Testresponse

Outgoing Query Message

query message

Inviamo un messaggio dal test al oggetto sotto test

Verifichiamo la risposta

Collaborator

Oggetto sotto test chiede qualcosa al collaboratore

Integration test?

Quando usare un test double?

• Valore: MAI !

• Entità: A volte

• Servizio: Sempre

Test Object Under Test

query messageresponse

Outgoing Query Message

Non facciamo asserzioni sul messaggio di query tra Object Under Test e collaboratore

Programmiamo il test double con una risposta fissa (stub)

Collaborator test double

@Test public void listManyBooks() { BookRepository bookRepository = context.mock(BookRepository.class); ListBooksUseCase = new ListBooksUseCase(bookRepository);

context.checking(new Expectations() {{ allowing(bookRepository).findAll(); will(returnValue(asList(new Book(), new Book(), new Book()))) }});

List<Book> books = useCase.listBooks();

assertThat(books.count(), is(3)); }

Esempio

Test Object Under Test

Outgoing Command Message

Inviamo un messaggio dal test al oggetto sotto test

Dobbiamo verificare che il comando in uscita sia inviato

Collaboratorcommand message

Oggetto sotto test invia un comando al collaboratore

Test Object Under Test

command message

Outgoing Command MessageCollaborator

MOCK

Impostiamo una expectation sul fatto che il messaggio sia inviato

@Test public void productFound() { ProductRepository productRepository = context.mock(ProductRepository.class); Display display = context.mock(Display.class); PointOfSale pos = new PointOfSale(productRepository, display);

context.checking(new Expectations() {{ allowing(productRepository).find(“__A_BARCODE__”); will(returnValue(new Product(“__PRODUCT_PRICE__”)));

oneOf(display).show(“__PRODUCT_PRICE__”); }});

pos.onBarcode(“__A_BARCODE__”); }

Esempio

Test Object Under Test

Message sent to self (private)

Non facciamo nessuna verifica sul messaggio privato

Perché usare i mock (test double)?

• Se ben usati generano enorme pressione sul design

• Vanno usati come strumento di design

• NON vanno usati al solo scopo di isolare i test

In brevissimo

• Curate i vostri test come curate il codice di produzione

• Fate code-review anche dei test!

• Un buon test oggi può salvarvi da un sacco di lavoro domani

Thank you!

Domande?