Riduildel, cher ami, ce qui est décrit dans l'article ne constitue pas des tests unitaires dans le sens où ils sont covariants au code alors qu'un test unitaire, écrit en TDD est par essence contravariant.
Dit autrement, un test unitaire n'a pas besoin d'être modifié lorsque le code change puisqu'il est découplé du code. Pour le dire autrement, cela fait plus de 10 ans que j'écris des tests unitaires et je n'ai réellement compris ce qu'il fallait faire et comment en écrire que depuis moins d'un an... #Craftsmanship
— Liens directs
Après plusieurs jours de recherche et quelques semaines de remise en question, je peux enfin mettre à plat ce que j'ai compris du clean code et surtout de ce que devrait être des TU écrits en TDD pour un service RESTful. Pour vous faire saisir l'idée dernière cela, je vais considérer que nous écrivons un serveur RESTful en Sparkjava (je n'ai pas encore migré totalement vers Javalin).
Selon Martin Fowler, Robert Martin et Kent Bent, nous n'avons pas forcément le lien MaClasse => MaClasseTest. En réalité, nous avons une problématique à laquelle répond une interface derrière laquelle se trouve une implémentation pouvant avoir un groupe de classes. Notre classe de test va donc tester ce petit groupe de classes à travers des TU.
Pourquoi ne pas conserver le pattern UneClasse <=> UneClasseTest ?
Simplement parce qu'il corrèle nos tests à la structure et l'organisation de notre code (ie. noms et packages). De manière formelle, on dit que le code des classes des tests et le code des sources sont covariants ; c'est-à-dire que changer l'architecture de l'un, induit inévitablement un changement chez l'autre.
Alors qu'en réalité, les TU écrits en TDD s'attachent à ce qui doit être fait et non comment cela est fait. De facto, tester l'implémentation à travers une interface décroit le couplage et rend le code de nos tests contra-variant avec le code de nos sources.
Considérons un micro-service RESTful qui gère un carnet d'adresse. La première question à se poser est : quelle est la liste des fonctionalités qu'expose ce service ?
Nous allons en considérer que deux :
=> Pas de recherche ni modification pour cet exemple.
En BDD, nous testerions ce service REST en lui envoyant du JSON ou carrément depuis l'interface graphique via du Selenium ou du Protractor où nos uses cases auraient été rédigés en Gherkin. Dans notre exemple en TDD, nous allons tester directement les méthodes des routes Sparkjava en mockant les paramètres spark.Request et spark.Response pour qu'ils retournent les valeurs qui nous arrangent bien.
L'achitecture demeure en couche, même si une encapsulation forte (au sens Yegor Bugayenko du terme) est omniprésente.
__________________
| |
| ROUTES |
| ^ |
|--------|---------
| v |
| PERSISTENCE |
| ^ |
|--------|--------|
| v |
| BASE DE DONNÉES |
|_________________|
Avant d'écrire la route AddPerson, nous allons commencer par écrire un test qui échoue :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
import org.testng.Test
import org.assertj.Assertions.assertThat
import org.mockito.Mock.mock
import org.mockito.Mock.when
import spark.Request
import spark.Response
class AddPersonTest {
private val route:Route = AddPerson()
@Test
fun `AddPerson route should be able to create a regular user in base`() {
// Given
val json = "{'name':'Wayne', 'firstName':'Bruce', 'mobile':'+33012345678' }"
val request = mock(Request::class.java)
val response = mock(Response::class.java)
when(request.body()).thenReturn(json)
// When
val result = route.handle(request, response)
// Then
assertThat(result).contains("id")
.contains("'name':'Wayne'")
.contains("'firstName':'Bruce'")
.contains("'mobile':'+33012345678'")
}
}
Les routes sont des objets dédiés à une préoccupation. Typiquement la route AddPerson s'écrirait de la façon suivante :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
import com.jsoniter.JsonIterator
import com.jsoniter.JsonStream
import spark.Request
import spark.Response
import com.itametis.sample.dto.PersonDto
import com.itametis.sample.entity.People
import com.itametis.sample.entity.People.Person
class AddPerson:Route {
companion object {
private const val CONTENT_TYPE = "Content-Type"
private const val RESPONSE_TYPE = "application/json"
}
data class PersonDto @JvmOverload construtor(
val id:Long? = null,
val name:String = "",
val firstName:String = "",
val mobile:String = "",
)
override fun handle(request:Request, response:Response):Any {
// Récupération du DTO
val dto:PersonDto = JsonIterator.deserialize(request.body(), Person::class.java)
// Enregistrement de la personne en base
val person:Person = People.createIt(
"name", dto.getName(),
"firstName", dto.getFirstName(),
"phone", dto.getPhone()
)
// Conversion en JSON de la personne fraîchement créée en base (avec son ID)
response.header(CONTENT_TYPE, RESPONSE_TYPE)
return JsonStream.serialize(person)
}
}
Quant à la déclaration de cette route dans Sparkjava, imaginons qu'elle soit accessible depuis deux URL :
Nous la déclarerions de la sorte :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
class Main {
companion object {
@JvmStatic
fun main(vararg params:String) {
val addRoute:Route = AddPerson()
Sparkjava.post("/api/person/contact", addRoute)
Sparkjava.post("/api/contact", addRoute)
}
}
}
Avec cette architecture nous remarquons plusieurs choses :
On peut améliorer les routes en les décorants par d'autres routes implémentant la même interface :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
class Main {
companion object {
@JvmStatic
fun main(vararg params:String) {
val addRoute:Route = LogRouteInvocation( // On log la requête quoi qu'il arrive
AuthorizationChecker( // Est-on autorisé à exécuter la route
DtoToJsonConverter( // Évite le 'return JsonStream.serialize(...)' et le 'response.header(...)'
AddPerson() // La route d'origine
)
)
)
Sparkjava.post("/api/person/contact", addRoute)
Sparkjava.post("/api/contact", addRoute)
}
}
}
]]>Gotcha !!!
Je recherchais depuis hier un article exposant clairement la différence entre des tests unitaires et des tests en TDD.
C'est plus clair à présent
— Liens directs
Merci à Nicouf pour le lien :
]]>"A paper first published in the Empirical Software Engineering journal reports: "TDD seems to be applicable in various domains and can significantly reduce the defect density of developed software without significant productivity reduction of the development team." The study compared 4 projects, at Microsoft and IBM that used TDD with similar projects that did not use TDD.
— Liens directs
Un très bon article qui compare les systèmes de preuve formelle vs les TU et les langages fonctionnels et orientés objets.
— Liens directs
De TDD vers BDD une présentation faite en reveal.js (pas mal).
— Liens directs
Un peu comme celui d'Octo, le blog de la société Arolla est vraiment très riche en conseils et bonnes pratiques. Je le coudifie
— Liens directs