I did this presentation at Swissquote
That was great. First time I've considered using gimp to do a presentation. Was a great idea.
download the pdf
samedi 20 décembre 2014
jeudi 1 mai 2014
You say that a value is not used, prove it
Introduction
Some time, we are doing tests by considering that some values are not relevant for the test so we put default value. But some time we actually rely on those default values to have our tests working. It can lead to hidden knowledge and false passed tests
For instance, if I create a class OldWomen. For my test instances It's reasonable to put 70 as a default value. It's an age that anyone (at least any developer) will consider as old.
Let say I have a medicine dispenser to test. I can have a story like this
Given a old woman with a cancer
when medicine dispenser is turned on
then the old woman remains in life
But for any reasons we decide that Women older than 80 always forget to plug in the medicine dispenser.
...our test will continue to work
but in fact if the old woman is older than 80, she will die. (Indeed it's a simplified world)
Your test is not relevant and can make old women died.
Improving your default values
- We are all using continuous integration, a same test in a normal team will probably be run more than 10 times a day (Most of the time just to say "Hello I'm here and I'm working fucking well").
- We cannot test all cases
- We haven't the brain enough big to foreseen all impacts of our changes (If we could, we basically won't do any test).
So let's make the machine do the job. Let's implement a default value generator !!!
By randomly generating default value you will probably failed one day (and probably the first day) If your code isn't safe for all values.
Obviously this approach has some limitation and some corner cases will not be threated.
Some ideas to implement those generators
- Create a generic class RandomValueGenerator<T>. You will be able to handle many cases this way
- When you create String value take care of alpha numeric/ASCII characters
- Generate nullable value if it's relevant (for example 50% of the values returned will be null ). You can create a decorator to do that
public static <U> RandomValueGenerator<U> nullable(final RandomValueGenerator<U> randomValueGenerator) {
- Generate zero value if it's relevant (for example 20% of the value returned will be 0) you can also create a decorator to handle that.
- Support enum and allow to have only a limited number of value RandomValueGenerator.fromPossibleValues(E... values)
- Use it every where in your unit test or acceptance test (How much entity builder do you have in your code ?)
- It's difficult to find the good ranges for values (Is it usefull to generate amount that are equals to 100000000000000 $ ?). But don't assert too mu
- You can also combine some random generator :
With a method RandomValueGenerator.combine(RandomValueGenerator... randomGenerator)you can have
RandomValueGenerator.combine(
RandomValueGenerator.fromPossibleValues(1.,0.,NaN),RandomValueGenerator.doubleValue())
Here it will provide you :
- half of the time a value that is known to be error prone
- half of the time a value that is actually random
Reproduce your tests
A good test should be reproductible. Fortunatly most of the random generator are initialize with a seed and given that seed all value are predictible. Then it's easy to have two mode :- By default, the seed is randomly chosen and we reinitialized the random generator at the beginning of all test to this seed
- In order to reproduce a failing test, if a property is set with the seed, we use this seed.
private final static Random random;You need to find a good way to call this newTest method on every tests. It's your job here !!!
static {
if (System.getProperty("test.random.seed") == null) {
seed = new Random().nextLong();
} else {
seed = Long.getLong(System.getProperty("test.random.seed"));
}
random = new Random(seed);
}
public static void newTest() {
LOGGER.info("running test with random generator initialized with seed " + seed);
random.setSeed(seed);
}
Non regression testing
At this point we tested that unexpected change in GIVEN values has no impact on THEN values but we also want to be sure that only expected values has changed on a test. Let's take again our old example:Given a old woman aged of 90 with a cancer
when medicine dispenser is turned on
then the old woman will die
But it's not only that the old woman will die that changed, there is also a good news, the electricity bill will not increase dramatically before people discover the old woman.
When you code do you always have in mind this kind of change ? It why non regression testing are so important. Although, I don't want to change my test just to handle that case (It probably doesn't worth it) but I want to acknowledge that this change in behavior is ok.
There is no easy solution to solve this problem, but here some ideas :
- You need to compare state of the system at the end of the test with the expected state. It could be difficult or simple depending on your system. If your are in a db oriented project you may store the resulting database after the test.
- For the first run you cannot test anything (It's non regression testing... ) but you should store the results of this first test in the source base of the project.
- For non regression testing always use the same seeds. You can use lot of different seed but for each seed you should expect to have the same result.
- Reuse the seeds that have in the past broken your test.
mercredi 31 octobre 2012
Le test builder
"Putain c'est quoi ce que tu testes avec ce code de merde que tu dis être un test unitaire"
C'est par cette douce pensée que j'envisage souvent les codes reviews des tests unitaires qui me sont soumis.
Moi j'aime bien les tests unitaires, je les respecte et j'essaye de les rendre heureux ou du moins utiles. Sauf que des fois, c'est le drame, une méthode à mocker toute tordue, dix scénarios de test pour la même méthode, le resultat à vérifier qui devient trop compliqué et le test devient trop complexe, illisible et donc un peu inutile.
D'ou un petit pattern que je me suis amusé à créer : le test builder
C'est une classe utilitaire permettant d'ecrire les scénarios de test unitaire pour une méthode à la mode DSL.
public Collection<ObjetAVendreAvecLeurPrix> queVendreAMémée(Mémée laMémée){
}
dans la classe VRPService
Vous l'aurez compris, cette méthode permet de connaître une liste d'object que l'on peux vendre à mémée et leur prix en fonction de la mémée.
Cette méthode est sans doute assez compliquée à tester et il faudra sans doute de nombreuses mémées différentes pour la tester correctement.
on aura par exemple les tests suivants :
testQueVendreAMémée_qandMéméeEstRicheEtGénéreuse
testQueVendreAMémée_qandMéméeEstRicheMaisRadine
testQueVendreAMémée_qandMéméeEstPauvreEtRadine
testQueVendreAMémée_qandMéméeEstPauvreEtGénéreuse
On supose aussi, pour les besoins de l'exemple, que la méthode queVendreAMémée utilise un service richesseDeMéméeService avec une méthode isMéméeRiche(Mémée laMémée)
Un des test unitaire resemblerait à ca
@Test
public void testQueVendreAMémée_qandMéméeEstRicheEtGénéreuse(){
RichesseDeMéméeService richesseDeMéméeService = mock(RichesseDeMéméeService.class);
Mémée méméeRicheEtGénéreuse = new Mémé();
mémée.setGénérosité(Mémée.Généreuse);
mémée.ajouteAuxObjetsQuePossedentMémée(ObjectAVendre.Aspirateur);
when(richesseDeMéméeService.isMéméeRiche(méméeRicheEtGénéreuse)).thenReturn(true);
Collection<ObjetAVendreAvecLeurPrix> resultats = new VRPService().QueVendreAMémée(méméeRicheEtGénéreuse);
assertEquals(1,resultats.size);
assertEquals(ObjectAVendre.Télévision,resultats.get(0).getObjectAVendre()); // ok get(0) n'existe pas dans Collection mais bon, les mémées riches et généreuses non plus et personne ne me fait chier avec ça
assertEquals(100.000,resultats.get(0).getPrix());
}
vous avez vu c'est assez chiant à lire et pourtant c'est assez clair je pense. Et puis pour tester la mémée pauvre ça va faire de la copie de code inutile.
Bref transformons ça avec notre test builder
tout d'abord on va ecrire une classe interne notre "test builder"
private static class QueVendreAMéméeTestBuilder{
Collection<ObjetAVendreAvecLeurPrix> resultats;
Mémée laMéméeATesté = new Mémée();
RichesseDeMéméeService richesseDeMéméeService = mock(RichesseDeMéméeService.class);
public QueVendreAMéméeTestBuilder méméeGénéreuse(Boolean généreuse){
laMéméeATesté.setGénérosité((généreuse)?Mémée.Généreuse:Mémée.Radine);
return this;
}
public QueVendreAMéméeTestBuilder méméePossède(ObjectAVendre object){
laMéméeATesté.ajouteAuxObjetsQuePossedentMémée(object);
return this;
}
public QueVendreAMéméeTestBuilder méméRiche(Boolean riche){
when(richesseDeMéméeService.isMéméeRiche(laMéméeATesté)).thenReturn(riche);
return this;
}
public QueVendreAMéméeTestBuilder regardeCeQueTuPeuxVendreAMémée(){
resultats = new VRPService().QueVendreAMémée(laMéméeATesté);
return this;
}
public QueVendreAMéméeTestBuilder nombreDobjetAVendre(int nb){
assertEquals(nb,resultats.size);
return this;
}
public QueVendreAMéméeTestBuilder onPeuxVendre(ObjectAVendre object, double aQuelPrix){
for (ObjectAVendre o : resultats){
if (o.getObjectAVendre().equals(object)){
assertEquals("Mémée peux acheter " + object + " a " o.getPrix() " et pas à " + aQuelPrix, aQuelPrix, o.getPrix());
return this;
}
}
assertFail("Mémée n'a jamais voulu acheter : " + object);
}
}
ensuite on va créer le test qui va avec
@Test
public void testQueVendreAMémée_qandMéméeEstRicheEtGénéreuse(){
new QueVendreAMéméeTestBuilder()
.méméeGénéreuse(true)
.méméeRiche(true)
.méméePossède(ObjectAVendre.Aspirateur)
.regardeCeQueTuPeuxVendreAMémée()
.nombreDobjetAVendre(1)
.onPeuxVendre(ObjectAVendre.Télévision,100.000);
}
et comme je suis généreux comme mémée je vous offre la version méméePauvreEtGénéreuse
@Test
public void testQueVendreAMémée_qandMéméeEstPauvreEtGénéreuse(){
new QueVendreAMéméeTestBuilder()
.méméeGénéreuse(true)
.méméeRiche(false)
.méméePossède(ObjectAVendre.Aspirateur)
.regardeCeQueTuPeuxVendreAMémée()
.nombreDobjetAVendre(1)
.onPeuxVendre(ObjectAVendre.ProgrammeTV,500);
}
Reste une question en suspens, que fait la mémé pauvre avec son programme TV sans TV ? Laissez vos commentaires.
C'est par cette douce pensée que j'envisage souvent les codes reviews des tests unitaires qui me sont soumis.
Moi j'aime bien les tests unitaires, je les respecte et j'essaye de les rendre heureux ou du moins utiles. Sauf que des fois, c'est le drame, une méthode à mocker toute tordue, dix scénarios de test pour la même méthode, le resultat à vérifier qui devient trop compliqué et le test devient trop complexe, illisible et donc un peu inutile.
D'ou un petit pattern que je me suis amusé à créer : le test builder
Le test builder c'est quoi ?
C'est une classe utilitaire permettant d'ecrire les scénarios de test unitaire pour une méthode à la mode DSL.
Heu... le test builder c'est quoi ?
bon d'accord, prenons un exemple concret. J'ai une méthode qui s'appelepublic Collection<ObjetAVendreAvecLeurPrix> queVendreAMémée(Mémée laMémée){
}
dans la classe VRPService
Vous l'aurez compris, cette méthode permet de connaître une liste d'object que l'on peux vendre à mémée et leur prix en fonction de la mémée.
Cette méthode est sans doute assez compliquée à tester et il faudra sans doute de nombreuses mémées différentes pour la tester correctement.
on aura par exemple les tests suivants :
testQueVendreAMémée_qandMéméeEstRicheEtGénéreuse
testQueVendreAMémée_qandMéméeEstRicheMaisRadine
testQueVendreAMémée_qandMéméeEstPauvreEtRadine
testQueVendreAMémée_qandMéméeEstPauvreEtGénéreuse
On supose aussi, pour les besoins de l'exemple, que la méthode queVendreAMémée utilise un service richesseDeMéméeService avec une méthode isMéméeRiche(Mémée laMémée)
Un des test unitaire resemblerait à ca
@Test
public void testQueVendreAMémée_qandMéméeEstRicheEtGénéreuse(){
RichesseDeMéméeService richesseDeMéméeService = mock(RichesseDeMéméeService.class);
Mémée méméeRicheEtGénéreuse = new Mémé();
mémée.setGénérosité(Mémée.Généreuse);
mémée.ajouteAuxObjetsQuePossedentMémée(ObjectAVendre.Aspirateur);
when(richesseDeMéméeService.isMéméeRiche(méméeRicheEtGénéreuse)).thenReturn(true);
Collection<ObjetAVendreAvecLeurPrix> resultats = new VRPService().QueVendreAMémée(méméeRicheEtGénéreuse);
assertEquals(1,resultats.size);
assertEquals(ObjectAVendre.Télévision,resultats.get(0).getObjectAVendre()); // ok get(0) n'existe pas dans Collection mais bon, les mémées riches et généreuses non plus et personne ne me fait chier avec ça
assertEquals(100.000,resultats.get(0).getPrix());
}
vous avez vu c'est assez chiant à lire et pourtant c'est assez clair je pense. Et puis pour tester la mémée pauvre ça va faire de la copie de code inutile.
Bref transformons ça avec notre test builder
tout d'abord on va ecrire une classe interne notre "test builder"
private static class QueVendreAMéméeTestBuilder{
Collection<ObjetAVendreAvecLeurPrix> resultats;
Mémée laMéméeATesté = new Mémée();
RichesseDeMéméeService richesseDeMéméeService = mock(RichesseDeMéméeService.class);
public QueVendreAMéméeTestBuilder méméeGénéreuse(Boolean généreuse){
laMéméeATesté.setGénérosité((généreuse)?Mémée.Généreuse:Mémée.Radine);
return this;
}
public QueVendreAMéméeTestBuilder méméePossède(ObjectAVendre object){
laMéméeATesté.ajouteAuxObjetsQuePossedentMémée(object);
return this;
}
public QueVendreAMéméeTestBuilder méméRiche(Boolean riche){
when(richesseDeMéméeService.isMéméeRiche(laMéméeATesté)).thenReturn(riche);
return this;
}
public QueVendreAMéméeTestBuilder regardeCeQueTuPeuxVendreAMémée(){
resultats = new VRPService().QueVendreAMémée(laMéméeATesté);
return this;
}
public QueVendreAMéméeTestBuilder nombreDobjetAVendre(int nb){
assertEquals(nb,resultats.size);
return this;
}
public QueVendreAMéméeTestBuilder onPeuxVendre(ObjectAVendre object, double aQuelPrix){
for (ObjectAVendre o : resultats){
if (o.getObjectAVendre().equals(object)){
assertEquals("Mémée peux acheter " + object + " a " o.getPrix() " et pas à " + aQuelPrix, aQuelPrix, o.getPrix());
return this;
}
}
assertFail("Mémée n'a jamais voulu acheter : " + object);
}
}
ensuite on va créer le test qui va avec
@Test
public void testQueVendreAMémée_qandMéméeEstRicheEtGénéreuse(){
new QueVendreAMéméeTestBuilder()
.méméeGénéreuse(true)
.méméeRiche(true)
.méméePossède(ObjectAVendre.Aspirateur)
.regardeCeQueTuPeuxVendreAMémée()
.nombreDobjetAVendre(1)
.onPeuxVendre(ObjectAVendre.Télévision,100.000);
}
et comme je suis généreux comme mémée je vous offre la version méméePauvreEtGénéreuse
@Test
public void testQueVendreAMémée_qandMéméeEstPauvreEtGénéreuse(){
new QueVendreAMéméeTestBuilder()
.méméeGénéreuse(true)
.méméeRiche(false)
.méméePossède(ObjectAVendre.Aspirateur)
.regardeCeQueTuPeuxVendreAMémée()
.nombreDobjetAVendre(1)
.onPeuxVendre(ObjectAVendre.ProgrammeTV,500);
}
Reste une question en suspens, que fait la mémé pauvre avec son programme TV sans TV ? Laissez vos commentaires.
mercredi 18 janvier 2012
developpement d'une librairie pour prioriser des traitements
Imaginons que vous travaillez pour une chaine de restauration rapide. A l'heure du rush, un bus rempli de chef d'état se gare sur votre parking et se disent "on se tapperais bien un ptit mc do". Des lors, vous vous dites que ça serait pas mal de les servir rapidement... mais quand même faut pas déconner les clients normaux c'est quand même eux qui ramène les pépetes le reste de l'année et il va bien falloir les servir sans qu'ils soient trop laisés.
Il y a quelques mois, j ai été confronté à ce genre de problématique. Sauf que ce n'était pas des sandwichs que je fournissais mais de jolis retour de méthode.
De là est né une petite librairie que vous n' avez jamais entendu parlé : priority queue proxy.
Le priority proxy c'est l' idée de rendre transparent la gestion de la priorisation des appels de services que ce soit au niveau consumer ou producer. Et comme son nom l' indique on fait tout ça avec un beau proxy java et des thread queues.
Tout ca marche merveilleusement bien avec un bon vieux spring mais pas que.
Petit exemple avec spring:
On crée l'éxecuteur, c'est lui qui s'occupe de dire qui s'execute ou
<bean id="priorityQueueProxyExecutor" class"....priorityExecutor.Executor">
<constructor-args value="10">
</bean>
L'argument du constructeur est la taille du tread pool partagé par tous les clients. C'est sur ce thread pool que sont gérées les priorités.
On créer les services :
<bean id="lowPriorityService" factory-bean="je vous l'explique plus tard">
<constructor-arg value="l'interface du service"/>
<constructor-arg><bean class="monImplementationDeService"></constructor-arg>
<constructor-arg>5</constructor-arg>
<constructor-arg>0</constructor-arg>
<constructor-arg>priorityQueueProxyExecutor</constructor-arg>
</bean>
<bean id="highPriorityService" ...
<constructor-arg value="l'interface du service"/>
<constructor-arg>...</constructor-arg>
<constructor-arg>10</constructor-arg>
<constructor-arg>5</constructor-arg>
<constructor-arg>priorityQueueProxyExecutor</constructor-arg>
</bean>
Explication, On a maintenant deux services différents qui représentent en réalité la même chose mais avec des priorité différentes :
Le premier service a une priorité de 5 et n'a pas de thread réservé pour lui tout seul.
Le deuxième service a une priorité de 10 et possede 5 slot privé qu'il est seul a utilisé. (Des caisses réservé pour les présidents dans le cas du McDo)
Tout ça est tres joli et ou je le telecharge me direz vous ? Bah soit vous venez travaillez avec moi dans ma boite, soit vous ne pouvez-pas.
Mais n' envisagez pas pour autant le suicide car je vous propose de faire la version 2 en LGPL avec des fonctionnalités à vous faire pousser des rouflaquettes:
Il y a quelques mois, j ai été confronté à ce genre de problématique. Sauf que ce n'était pas des sandwichs que je fournissais mais de jolis retour de méthode.
De là est né une petite librairie que vous n' avez jamais entendu parlé : priority queue proxy.
Le priority proxy c'est l' idée de rendre transparent la gestion de la priorisation des appels de services que ce soit au niveau consumer ou producer. Et comme son nom l' indique on fait tout ça avec un beau proxy java et des thread queues.
Tout ca marche merveilleusement bien avec un bon vieux spring mais pas que.
Petit exemple avec spring:
On crée l'éxecuteur, c'est lui qui s'occupe de dire qui s'execute ou
<bean id="priorityQueueProxyExecutor" class"....priorityExecutor.Executor">
<constructor-args value="10">
</bean>
L'argument du constructeur est la taille du tread pool partagé par tous les clients. C'est sur ce thread pool que sont gérées les priorités.
On créer les services :
<bean id="lowPriorityService" factory-bean="je vous l'explique plus tard">
<constructor-arg value="l'interface du service"/>
<constructor-arg><bean class="monImplementationDeService"></constructor-arg>
<constructor-arg>5</constructor-arg>
<constructor-arg>0</constructor-arg>
<constructor-arg>priorityQueueProxyExecutor</constructor-arg>
</bean>
<bean id="highPriorityService" ...
<constructor-arg value="l'interface du service"/>
<constructor-arg>...</constructor-arg>
<constructor-arg>10</constructor-arg>
<constructor-arg>5</constructor-arg>
<constructor-arg>priorityQueueProxyExecutor</constructor-arg>
</bean>
Explication, On a maintenant deux services différents qui représentent en réalité la même chose mais avec des priorité différentes :
Le premier service a une priorité de 5 et n'a pas de thread réservé pour lui tout seul.
Le deuxième service a une priorité de 10 et possede 5 slot privé qu'il est seul a utilisé. (Des caisses réservé pour les présidents dans le cas du McDo)
Tout ça est tres joli et ou je le telecharge me direz vous ? Bah soit vous venez travaillez avec moi dans ma boite, soit vous ne pouvez-pas.
Mais n' envisagez pas pour autant le suicide car je vous propose de faire la version 2 en LGPL avec des fonctionnalités à vous faire pousser des rouflaquettes:
- Gestion de pool de thread privé plus fine.
- Meilleure gestion de la concurence (utilisation eventuel des packages java.util.concurent).
- Tread affinity pour une configuration intelligente des threads.
- Gestion des priorités même sur une ferme de provider de service (un cloud pour faire jeune)
- Gestion des priorités changeantes (Finalement les types dans le bus etaient certes des chefs d' état mais surtout d'anciens dictateurs balayés par les révolutions arabes)
- Configuration des priorités au runtime...
- D'autres idées m' étaient venues mais je mettrais à jour plus tard car ça ne vient plus
Alors tenté par une
aventure open source ?
vendredi 30 décembre 2011
Supporting i18n inside Javascript backend JAVA
Résumé :
Quand je code du javascript avec du .jsp (ou .gsp ou .ceQueJeVeux) j'aime bien écrire mon code dans une classe javascript. Mais quand je commence à toucher à l'internationalisation, je suis tenté (30 secondes) de mettre mon javascript tout moche dans mes belles pages afin de pouvoir utiliser les resources i18n.Cet Article va vous montrer trois façons différentes de pouvoir gérer cette problématique plus ou moins proprement. La troisieme technique vous permettra de plus de briller en société (ou pas).
J'exclus toute idée de faire de l'ajax pour faire ces traduction parce que franchement ça peut etre vite crade et faire plein de requêtes pas franchement utiles.
1 - Du javascript dans mes pages...
La premiere idée est la plus simple mais sans doute la moins bonne. L'idée est de faire une page qui contient que les traductions des éléments javascript et l'importer dans nos pages (quelque soit la technique)Une telle page peut resembler à ca :
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<script type="text/javascript">
var i18n = new Object();
i18n.disclaimerTitle = "<fmt:message key="disclaimer.title"/>";
i18n.cancelDisclaimer = "<fmt:message key="disclaimer.cancelButton"/>";
i18n.confirmDisclaimer = "<fmt:message key="disclaimer.confirmButton"/>";
i18n.serverTimeOut = "<fmt:message key="serverTimeOut"/>";
i18n.pleaseWait = "<fmt:message key="pleaseWait"/>";
</script>
Ensuite dans son javascript on à plus qu'a utiliser notre objet par exemple :
<script type="text/javascript">
alert(i18n.disclaimerTitle);
</script>
C'est bien parce que :
- C'est simple à mettre en place.
C'est nul parce que :
- On récupère du coup toutes les traductions même si on en a pas besoin.
- Nos javascript ne fonctionnent pas sans la traduction (ca peut etre résolu je vous laisse réfléchir comment)
2- Du javascript généré à la volée.
L'idée c'est d'appeler le javascript en passant par une servlet (ou un autre truc du genre).Notre servlet à plusieurs responsabilités.
- Récupérer la langue (ça peut être un paramètre de la requête)
- Récupérer le javascript non traduit
- Traduire les parties à traduire
- Renvoyer le fichier
- Eventuellement, géré un cache
protected String translateJS(String jsString, String lang) {
Pattern pattern = Pattern.compile("i18n\\{[a-zA-Z0-9.]*\\}");
Matcher matcher = pattern.matcher(jsString);
Boolean find = false;
StringBuffer translatedJsString = new StringBuffer();
while (matcher.find()) {
find = true;
String key = matcher.group().substring(5, matcher.group().length() - 1);
matcher.appendReplacement(translatedJsString, translate(key, lang));
}
if (!find) {
translatedJsString.append(jsString);
}
return translatedJsString.toString();
}
Le test unitaire qui va avec pour mieux comprendre :
public class TestI18nJavascriptServlet {
private I18nJavascriptServlet i18nJavascriptServlet;
@Before
public void initMock() {
i18nJavascriptServlet = Mockito.spy(new I18nJavascriptServlet());
Mockito.doReturn("test1-en").when(i18nJavascriptServlet).translate(Mockito.eq("test1"), Mockito.eq("en"));
Mockito.doReturn("test2-en").when(i18nJavascriptServlet).translate(Mockito.eq("test2"), Mockito.eq("en"));
}
@Test
public void testNoTranslation() {
String js = "no translation'";
Assert.assertEquals(js, i18nJavascriptServlet.translateJS(js, "en"));
}
@Test
public void testSimpleTranslation() {
String js = "translation for i18n{test1}'";
Assert.assertEquals("translation for test1-en", i18nJavascriptServlet.translateJS(js, "en"));
}
@Test
public void testMultiTranslation() {
String js = "translation for i18n{test1} and i18n{test2}'";
Assert.assertEquals("translation for test1-en and test2-en", i18nJavascriptServlet.translateJS(js, "en"));
}
}
C'est bien parce que :
- On récupere les traductions au besoin.
- Les pages javascript sont fonctionnelles en static mais pas top (on à pas de traduction par défaut. Quoi qu'on peut reprendre l'idée du chapitre suivant).
C'est nul parce que :
- C'est un peut chiant à mettre en place.
- Ca me permet pas de briller en société
3- Du javascript généré à la volée version spring EL.
C'est presque la même solution que la précédente mais elle devient un peu plus élégante. Au lieu de faire du parsing comme précédement, on va utiliser les expression langage de Spring (SpEL)Le code type POC que j'explique apres:
public class TemplateParserContext implements ParserContext {
public String getExpressionPrefix() {
return "i18n(";
}
public String getExpressionSuffix() {
return ")";
}
public boolean isTemplate() {
return true;
}
}
@Test
public void elParser() throws SecurityException, NoSuchMethodException {
String jsTextToTranslate = "alert(i18n.translate('hello','Salut les filles')))";
jsTextToTranslate = jsTextToTranslate.replaceAll("i18n.translate", "i18n(#translate");
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.registerFunction("translate", TestI18NSpelJavascript.class.getDeclaredMethod("translate", String.class, String.class));
String translated = parser.parseExpression(jsTextToTranslate, new TemplateParserContext()).getValue(context, String.class);
Assert.assertEquals("alert(\"translated:hello->Salut les filles\")", translated);
}
public static String translate(String key, String defaultValue) {
return '\"' + "translated:" + key + "->" + defaultValue + '\"';
}
WTF ? me direz vous... - l'inner classe me sert à définir pour SPel l'endroit ou chercher les expression langages : dans mon cas je dis que c'est dans une chaine i18n(monEl)
- j'enregistre une function translate pour le parser SpEL qui comme son nom l'indique permet de faire le travail de traduction.
- La transformation du javascript avec le replaceAll me permet d'avoir du coté javascript original : i18n.translate(maclef,matraductionparDefault) et du coté du javascript à transformer via el : i18n(#translate(maclef,matraductionparDefault)). Car coté SpEL, une fonction commence toujours par # (c'est le code antlr généré donc y a pas moyen de modifier ça à la volée)
- je fais gaffe que ma méthode translate me renvoie une string au sens js. (les petites quotes qui vont bien)
- J'importe dans ma page original "i18n.js" qui défini un objet i18n qui possède la fonction translate qui renvoie la défault value
C'est bien parce que :
- On récupere les traductions au besoin.
- Les pages javascript sont fonctionnelles en static.
- On peut envisager de faire d'autres choses via SpEL même si je n'ai pas d'idée comme ça. (si on s'en fous que le js ne marche plus en static sinon ... )
- Ca me permet de briller en société
C'est nul parce que :
- Il faut spring.
- C'est quand même un petit peu beaucoup
mercredi 14 décembre 2011
Tdd is not enough
Je viens de faire un petit truc en TDD et je me suis rendu compte que mon code était tout crade par rapport à ce qu'on pouvait faire.
L'idée c'était de faire une methode qui transforme les sauts de ligne ASCII en paragraphe ou en saut de ligne HTML
ex
Lalala
lilili
toto
tata
se transforme en
<p>Lalala<br/>Lilili</p><p>toto<br/>tata</p>
c'est fou non ?
Bref pour faire ça je me suis dis que j'allais sortir mon copain le TDD. Alors je fais mes tests au fur et à mesure du dev et j'obtiens ce jeu de tests
@Test
public void testFormatDisclaimerForHTMLPrinting() {
Assert.assertEquals("<p>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting(" \ntest"));
Assert.assertEquals("<p>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting(" \ntest\n"));
Assert.assertEquals("<p>test<br/>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting(" \ntest\ntest"));
Assert.assertEquals("<p>test</p><p>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting(" \ntest\n \ntest"));
Assert.assertEquals("<p>test</p><p>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting(" \ntest\n \n \n \ntest"));
Assert.assertEquals("<p>test</p><p>test</p>", DisclaimerAction.formatDisclaimerForHTMLPrinting(" \ntest\n \n \n \ntest"));
Assert.assertEquals("<p>test</p><p>test<br/>toto</p>",
DisclaimerAction.formatDisclaimerForHTMLPrinting(" \ntest\n \n \n \ntest\ntoto"));
}
et surtout ce code tout crade :
public static String formatDisclaimerForHTMLPrintingOld(String toFormat) {
String[] lines = toFormat.trim().split("\n");
String toReturn = "<p>";
for (int i = 0; i < lines.length; i++) {
if (StringUtils.isEmpty(lines[i].trim())) {
toReturn += "</p><p>";
while (i < lines.length - 1 && StringUtils.isEmpty(lines[i + 1].trim())) {
i++;
}
} else {
toReturn += lines[i];
if (i + 1 < lines.length && !StringUtils.isEmpty(lines[i + 1].trim())) {
toReturn += "<br/>";
}
}
}
return toReturn + "</p>";
}
J'ai pas trop essayer de reflechir en écrivant ça. Juste de prendre le test qui marche pas et d'écrire le code qui le fait marcher.
Alors ce matin je regarde d'un oeil coupable ce code, je commence à réfléchir et j'écris ça :
public static String formatDisclaimerForHTMLPrinting(String toFormat) {
String toReturn = toFormat.trim();
toReturn = toReturn.replaceAll("\\s*\\n\\s*\\n\\s*", "</p><p>");
toReturn = toReturn.replaceAll("\\n", "<br/>");
return "<p>" + toReturn + "</p>";
}
plus concis, plus simple, plus joli... et il passe bien mes tests.
Dans le TDD y a bien cette phase de refactoring qui est précisée cependant c'est quand même un peu difficile parce qu'une fois que j'ai écris mon code crade j'ai beau temps de le laisser comme ça s'il y a une autre urgence.
hum, je ne sais quelle conclusion tirer de tout ça. Si ce n'est que l'approche originale de TDD est peut être un peu trop dogmatique et qu'il faut toujours réfléchir avant de coder (... arf toujours pas de solution contre ce mal).
jeudi 15 septembre 2011
@Ignore c'est le mal
Mais c’est des fois bien pratique, notamment quand un test junit dépends d’une ressource (au sens large : BD, décision business,…) non disponible pour un temps (On me dira qu’il ne s’agit pas d’un test unitaire dans ce cas mais bon on peut faire des tests fonctionnels avec junit)
Bref il nous arrive de l’utiliser mais on l’utilise mal car on ne revient pas forcement dessus.
Bref il nous arrive de l’utiliser mais on l’utilise mal car on ne revient pas forcement dessus.
D’où l’idée de IgnoreTestSupport, le but est de centraliser les ignore à un endroit unique et de donner une date butoir à ses ignores.
1. On crée un fichier de configuration dans ignoreTests.properties
# We stop this test for 4 weeks…La syntaxe est simple ignore.class.”le nom de la classe”=date, ignore.package.”le nom du package”=date ou ignore.test.” le nom de la classe avec le nom de la méthode”=date 2. On rajoute la ligne suivante dans son fichier junit
ignore.class.org.javabien.utils.test.TestTestIgnore=2011/15/10
@Rule3. On n’oublie pas de rajouter le jar dans son classpath... IgnoreTest-1.0.jar
public IgnoreTestSupport ignoreTestSupport = new IgnoreTestSupport();
Inscription à :
Articles
(
Atom
)