J'ai aussi résumé un peu plus de convention dans "J'ai essayé d'organiser les critères de création UT en Java" ..
L'année dernière, j'ai écrit un article sur le blog intitulé "7 règles pour les tests unitaires normaux". Il y a eu plus de réponses que ce à quoi je m'attendais. Ce que j'ai écrit dans cet article n'est que le principe / principe, et certaines techniques sont nécessaires pour le réaliser.
En particulier, même si vous faites une telle règle et adhérez strictement à «écrire un test unitaire» Sans connaître la technique appropriée, la maintenance peut être difficile, la qualité peut ne pas être apportée et les déchets dont l'exécution est médiocre peuvent être produits en masse.
Ensuite, ce qu'il faut faire est de «concevoir le code original depuis le début afin que le test unitaire soit facile à écrire». alors. La première chose que vous devez apprendre n'est pas «comment écrire du code de test» mais «comment écrire du code de produit», c'est-à-dire «comment écrire du code de produit». En outre, le "depuis le début" mentionné ici est un niveau que vous pouvez faire correctement avant de le donner à des personnes telles que la révision et la fusion des relations publiques sans dire à fond "C'est le test d'abord! Faites TDD!" ..
Cependant, comme il s'agit d'un léger changement de mentalité, je pense qu'il est plus facile pour les débutants de voir un exemple concret, j'ai donc fait un exemple typique comme aide-mémoire.
L'idée de base est de «séparer la logique métier et les E / S» et «renvoyer la même valeur quel que soit le nombre de répétitions». Voici un exemple concret basé sur cette idée.
Testez une méthode qui renvoie simplement une chaîne, séparant la sortie, telle que System.out.println
et Logger.info
, de la partie" assembler la sortie ".
public class Example01Good {
public static void main(String[] args) {
String message = args[0];
System.out.println(makeMessage(message));
}
static String makeMessage(String message) {
return "Hello, " + message;
}
}
@Test
public void testMakeMessage() {
assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}
Étant donné que la valeur d'un nombre aléatoire change à chaque fois, transmettez la valeur en tant qu'argument séparément de la logique métier. Un programme qui détermine si les dés sont pairs ou impairs ressemble à ceci.
public class Example02Good {
public static void main(String[] args) {
System.out.println(check(throwDice(Math.random())));
}
static String check(int num) {
return (num % 2 == 0) ? "Win" : "Lose";
}
static int throwDice(double rand) {
return (int) (rand * 6);
}
}
@Test
public void testCheck() {
assertThat(Example02Good.check(1), is("Lose"));
assertThat(Example02Good.check(2), is("Win"));
assertThat(Example02Good.check(3), is("Lose"));
assertThat(Example02Good.check(4), is("Win"));
assertThat(Example02Good.check(5), is("Lose"));
assertThat(Example02Good.check(6), is("Win"));
}
Faites-le donné de l'extérieur comme dans le cas des nombres aléatoires. Comme autre solution, comme dans l'exemple, créez une fabrique pour la génération de date et utilisez un mannequin (stub) pour les tests.
class SystemDate {
public LocalDate today() {
return LocalDate.now();
}
}
public class Example03Good {
SystemDate systemDate = new SystemDate();
public LocalDate tomorrow2() {
return systemDate.today().plusDays(1);
}
}
@Test
public void testTomorrow2() {
Example03Good target = new Example03Good();
target.systemDate = new SystemDate() {
@Override
public LocalDate today() {
return LocalDate.of(2017, 1, 16);
}
};
assertThat(target.tomorrow2(), is(LocalDate.of(2017, 1, 17)));
}
Fondamentalement, l'entrée / sortie de fichier doit être une méthode qui gère les chaînes de caractères, mais Dans certains cas, les données réelles sont très volumineuses ou doivent être traitées comme des "lignes". Dans ce cas, vous pouvez effectivement vérifier la lecture et l'écriture du fichier, mais comme le coût de description et le coût d'exécution sont élevés, Séparez la gestion directe des fichiers de la logique, comme Reader / Writer et InputStream / OutputStream Il est facile de programmer l'interface et d'utiliser StringReader / StringWriter etc. pour les tests.
public class Example04Good {
public static void main(String[] args) throws Exception {
System.out.println("hello");
try (Reader reader = Files.newBufferedReader(Paths.get("intput.txt"));
Writer writer = Files.newBufferedWriter(Paths.get("output.txt"));) {
addLineNumber(reader, writer);
}
}
static void addLineNumber(Reader reader, Writer writer) throws IOException {
try (BufferedReader br = new BufferedReader(reader);
PrintWriter pw = new PrintWriter(writer);) {
int i = 1;
for (String line = br.readLine(); line != null; line = br.readLine()) {
pw.println(i + ": " + line);
i += 1;
}
}
}
}
@Test
public void testAddLineNumber() throws Exception {
Writer writer = new StringWriter();
addLineNumber(new StringReader("a\nb\nc\n"), writer);
writer.flush();
String[] actuals = writer.toString().split(System.lineSeparator());
assertThat(actuals.length, is(3));
assertThat(actuals[0], is("1: a"));
assertThat(actuals[1], is("2: b"));
assertThat(actuals[2], is("3: c"));
}
Pourquoi écrire cela, pas seulement un exemple concret? Je vais également expliquer cela.
Encore une fois, les bases sont la séparation de la logique et des E / S. Voici quelques conseils pour tester. Prenons "un programme qui traite les caractères reçus des arguments de ligne de commande et les sort en standard" comme exemple.
public class Example01Bad {
public static void main(String[] args) {
String message = args[0];
// String message = "World"; //Pour le contrôle de fonctionnement
System.out.println("Hello, " + message);
}
}
N'est-ce pas le premier code que beaucoup de gens écrivent? Le code commenté est sympa. Écrivons un test unitaire pour ce programme.
@Test
public void testMain() {
String[] args = {"world"};
Example01Bad.main(args);
}
C'est comme ça? Il n'y a aucune affirmation! Donc, que ce programme soit normal ou non, c'est ** "les humains n'ont pas d'autre choix que de juger visuellement" ** même s'il s'agit de JUnit.
Vous pourriez penser, "Personne n'écrit ce genre de code, www.", Mais j'ai vu ce genre de "code de test qui fonctionne juste" plusieurs fois avec une logique métier plus compliquée.
Eh bien, c'est hors de question, mais une personne un peu plus consciente écrit:
/**
*Test inutile du système hautement conscient
*/
@Test
public void testMainWithSystemOut() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
String[] args = {"world"};
Example01Bad.main(args);
assertThat(out.toString(), is("Hello, world" + System.lineSeparator()));
}
J'accroche la sortie standard et compare les résultats. Ce test répond correctement aux exigences du test, rien n'est faux, mais c'est simplement inutilement compliqué. Je vais donc simplifier les choses en modifiant le code d'origine.
public class Example01Good {
public static void main(String[] args) {
String message = args[0];
System.out.println(makeMessage(message));
}
static String makeMessage(String message) {
return "Hello, " + message;
}
La "logique de traitement des chaînes de caractères" a été découpée en makeMessage. Ensuite, le test unitaire ressemble à ceci.
@Test
public void testMakeMessage() {
assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}
C'est devenu très simple, n'est-ce pas? Ça fait du bien.
Cependant, certaines personnes peuvent penser que "je n'ai pas pu tester la fonction d'affichage des caractères à l'écran!" C'est vrai. Cependant, il n'est pas nécessaire de confirmer une telle chose par un test unitaire.
Le test unitaire le plus important sur lequel se concentrer est le test logique. Il n'est pas nécessaire d'effectuer des tests déjà bien confirmés tels que l'entrée / sortie de fichier, l'entrée / sortie standard ou la sortie de journal.
Si vous créez un tel fichier IO etc. et que sa qualité n'est pas garantie, Comme la qualité doit être garantie en testant le fichier IO créé par vous-même, il n'est pas nécessaire de le vérifier par un traitement individuel utilisant la fonction. Le test dans une telle combinaison est également important, mais il est effectué dans une autre phase telle que le test d'intégration.
Par conséquent, il est important de toujours être conscient de la création d'une logique métier qui renvoie simplement la valeur de retour en décomposant la fonction aussi petite que possible comme cette fois. Il est recommandé que les E / S et l'initialisation des valeurs incontrôlables décrites ci-dessous soient poussées vers la couche contrôleur autant que possible, et que les tests y soient supprimés de l'UT qui est exécuté chaque fois que la génération est effectuée. Pour la maintenance du code hérité, il peut être inévitable de raccorder la sortie standard comme indiqué dans le mauvais exemple, mais ce n'est pas nécessaire si vous l'écrivez vous-même.
Il est également important de ne pas initialiser des éléments incontrôlables tels que des dates, des nombres aléatoires ou des réseaux (comme l'appel WebAPI) dans la méthode testée, comme exemples typiques.
Par exemple, créez une méthode de demain qui demande demain.
public class Example03Bad {
public LocalDate tomorrow() {
return LocalDate.now().plusDays(1);
}
}
Bien sûr, si vous initialisez LocalDate directement dans la méthode de demain comme celle-ci, la valeur changera tous les jours, donc le test automatique est impossible.
C'est un problème assez sérieux, et certaines personnes disent: «Je ne sais pas comment écrire un test unitaire», car je n'ai pas correctement éliminé ces dépendances.
La solution la plus simple consiste à passer la date en argument et à la séparer de la logique métier.
public LocalDate tomorrow(LocalDate today) {
return today.plusDays(1);
}
Ensuite, le test peut être écrit avec une valeur fixe comme celle-ci.
@Test
public void testTomorrow() {
Example03Good target = new Example03Good();
assertThat(target.tomorrow(LocalDate.of(2017, 1, 16)), is(LocalDate.of(2017, 1, 17)));
}
En gros, c'est bien, mais il est parfois plus facile d'utiliser le modèle et les stubs de la méthode d'usine quand ils sont nombreux. Tout d'abord, créez une classe SystemDate qui génère LocalDate, utilisez LocalDate.now dedans et définissez-la dans le champ Example03Good. La logique métier obtient le LocalDate via la classe SystemDate, ce qui est retourné dépend donc de l'implémentation. Le code produit est LocalDate.now.
class SystemDate {
public LocalDate today() {
return LocalDate.now();
}
}
public class Example03Good {
SystemDate systemDate = new SystemDate();
public LocalDate tomorrow2() {
return systemDate.today().plusDays(1);
}
}
Le code de test écrase le systemDate avec un stub comme celui-ci.
@Test
public void testTomorrow2() {
Example03Good target = new Example03Good();
target.systemDate = new SystemDate() {
@Override
public LocalDate today() {
return LocalDate.of(2017, 1, 16);
}
};
assertThat(target.tomorrow2(), is(LocalDate.of(2017, 1, 17)));
}
L'astuce ici est de placer les champs Factory tels que SystemDate au niveau du package ou supérieur, et non privés. Ensuite, vous pouvez changer d'implémentation sans utiliser de conteneur DI ou de framework Mock. Bien sûr, il est normal de le rendre privé et de le passer dans le constructeur, ou de le placer dans le setter, mais à la fin, il est toujours possible de le changer, il vaut donc mieux être facile.
Eh bien, je l'ai écrit brièvement, mais comment est-ce? J'espère que vous avez trouvé qu'un peu d'ingéniosité de conception rend le test beaucoup plus facile à écrire.
Ce n'est pas difficile, mais je fais souvent la même remarque aux débutants, et je ne connaissais pas le matériel qui résume de ce point de vue, alors je l'ai écrit. D'ailleurs, on dit que «le TDD (développement piloté par les tests) contribue à la qualité» car ce type d'écriture est naturellement forcé.
De plus, lors de l'étude d'un langage fonctionnel, certains points semblent être le summum de ces méthodes de conception, je pense donc que de nombreux points peuvent être utiles.
Alors cette année aussi Happy Hacking!
Recommended Posts