Unit Testing mit vielen Bedingungen - Best Practice?

JuriW

Aktives Mitglied
Ich bin ein Anfänger bezüglich Unit-Tests. Ich würde gerne Unit Tests für viele Key Value Paare schreiben, von denen die Values oftmals gleiche Bedingungen haben. Beispiel:
Ich habe viele kleine String Arrays, wobei der erste Wert ein Key ist und der zweite Wert ein Value (Ja, ich weiß, dass es bessere Implementierungen gibt).
Java:
public static void main(String[] args) {

    String[] kv1 = {"se", "Server is running"};
    String[] kv2 = {"ap", "App is running"};
    String[] kv3 = {"pr", "Printer is running"};
    String[] kv4 = {"ra", "Radio is running"};
    String[] kv5 = {"ws", "Website is not available"};
    /*
     *
     * weitere kv-paare
     *
     */
    ArrayList<String[]> pairs = new ArrayList<>(Arrays.asList(kv1, kv2, kv3, kv4, kv5));
    for (String[] kv : pairs) {
        isValueValid(kv);
    }
}

public static boolean isValueValid(String[] pair) {

    if (pair[0].equals("se") || pair[0].equals("ab") || pair[0].equals("pr")) {
        if (pair[1].length() > 50) {
            System.out.println("Value is shouldn't contain more than 50 chars");
            return false;
        }
    } else if (pair[0].equals("ra") || pair[0].equals("ws")) {
        if (pair[1].length() > 100) {
            System.out.println("Value is shouldn't contain more than 100 chars");
            return false;
        }
    }
    /*
     *
     * mehr Bedingungen für andere kv-paare
     *
     */
    return true;
}

Die Sache ist, dass das CI System nur den Methodennamen von dem Test ausgibt, wenn er fehlt schlägt.

Heißt, ich muss jeden key als einzelnen Test, mit sprechendem Namen schreiben:
Java:
 private String[] pair = new String[2];

    @Test
    public void isServerValueValid(){
        pair[0] = "se";
        int criteria = 50; // defines maximum length

        pair[1] = "";
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = "abcdef";
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria);
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria+1);
        assertEquals(false, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria + 20);
        assertEquals(false, TestKlasse.isValueValid(pair));

    }

    @Test
    public void isAppValueValid(){
        pair[0] = "ab";
        int criteria = 50; // defines maximum length

        pair[1] = "";
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = "abcdef";
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria);
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria+1);
        assertEquals(false, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria + 20);
        assertEquals(false, TestKlasse.isValueValid(pair));
    }

    @Test
    public void isPrinterValueValid(){
        pair[0] = "pr";
        int criteria = 50; // defines maximum length

        pair[1] = "";
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = "abcdef";
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria);
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria+1);
        assertEquals(false, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria + 20);
        assertEquals(false, TestKlasse.isValueValid(pair));
    }

    @Test
    public void isRadioValueValid(){
        pair[0] = "ra";
        int criteria = 100; // defines maximum length

        pair[1] = "";
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = "abcdef";
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria);
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria+1);
        assertEquals(false, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria + 20);
        assertEquals(false, TestKlasse.isValueValid(pair));
    }

    @Test
    public void isWebsiteValueValid(){
        pair[0] = "ws";
        int criteria = 100; // defines maximum length

        pair[1] = "";
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = "abcdef";
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria);
        assertEquals(true, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria+1);
        assertEquals(false, TestKlasse.isValueValid(pair));
        pair[1] = generateValueData(criteria + 20);
        assertEquals(false, TestKlasse.isValueValid(pair));
    }

    private String generateValueData(int length){
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < length; i++){
            sb.append("a");
        }
        return sb.toString();
    }


Meine Fragen sind nun:
1. Ist das der richtige Ansatz zum testen?
2. Wie kann ich in meinem Code in der Funktion isValueValid die if-else if-Blöcke schöner gestalten?
 
Die isValueValid() Methode könntest du vereinfachen, indem du die Entscheidungen, wann ein Wert zu lang ist, in eine Map-Datenstruktur kodierst:
Java:
private static final Map<String, Integer> MAX = Map.of(
    "se", 50,
    "ab", 50,
    "pr", 50,
    "ra", 100,
    "ws", 100
);
public static boolean isValueValid(String[] pair) {
  return !MAX.containsKey(pair[0])
       || MAX.get(pair[0]) >= pair[1].length();
}
(Utility-Methoden sollten bevorzugt nichts auf stdout/stderr ausgeben. Hier könntest du eher mit Exceptions arbeiten.)
Außerdem: Warum verwendest du ein String[] Array und nicht einfach zwei String-Parameter?

Bezüglich Unit-Test:
Google mal nach "<dein-unit-framework> parameterized tests".
Für JUnit 4 etwa: https://github.com/junit-team/junit4/wiki/parameterized-tests
oder TestNG: https://testng.org/doc/documentation-main.html#parameters-dataproviders
 
An die HashMap dachte ich auch schon. Das Problem ist aber, dass Kriterien nicht nur aus Zahlen bestehen, sondern zum Beispiel auch aus einem Zahlenbereich (50-100) oder nur aus bestimmten, einzelnen Buchstaben bestehen dürfen. Hast du dafür auch noch eine Idee?

Und danke für die Links! Mein Framework ist JUnit, wahlweise hamcrest oder AssertJ

Warum verwendest du ein String[] Array und nicht einfach zwei String-Parameter?
Ich kriege in der Funktion vor meiner eigentlichen Funktion die Parameter als String[]. Ich hab es einfach mal so beibehalten.
 
An die HashMap dachte ich auch schon. Das Problem ist aber, dass Kriterien nicht nur aus Zahlen bestehen, sondern zum Beispiel auch aus einem Zahlenbereich (50-100) oder nur aus bestimmten, einzelnen Buchstaben bestehen dürfen. Hast du dafür auch noch eine Idee?
Du kannst die Bedingungen als Predicate<String> abbilden. Dann kannst du dir für jede Bedingung/Prüfung eine parametrisierte Methode schreiben, die diese Prüfung ausdrückt. Z.B.:
Java:
import java.util.*;
import java.util.function.*;
...
private static final Map<String, Predicate<String>> CONDITIONS = Map.of(
    "ws", maxLength(100),
    "xx", rangeLength(10, 20),
    "yy", containsOnly('a', 'b', 'x') // <- characters MUST be in ascending order!
);
private static Predicate<String> maxLength(int length) {
  return s -> s.length() <= length;
}
private static Predicate<String> rangeLength(int min, int max) {
  return s -> s.length() >= min && s.length() <= max;
}
private static Predicate<String> containsOnly(char... chars) {
  return s -> s.chars().allMatch(c -> Arrays.binarySearch(chars, (char) c) > -1);
}
public static boolean isValueValid(String[] pair) {
  return !CONDITIONS.containsKey(pair[0])
       || CONDITIONS.get(pair[0]).test(pair[1]);
}
 
Java:
import java.util.*;
import java.util.function.*;
...
private static final Map<String, Predicate<String>> CONDITIONS = Map.of(
    "ws", maxLength(100),
    "xx", rangeLength(10, 20),
    "yy", containsOnly('a', 'b', 'x') // <- characters MUST be in ascending order!
);
private static Predicate<String> maxLength(int length) {
  return s -> s.length() <= length;
}
private static Predicate<String> rangeLength(int min, int max) {
  return s -> s.length() >= min && s.length() <= max;
}
private static Predicate<String> containsOnly(char... chars) {
  return s -> s.chars().allMatch(c -> Arrays.binarySearch(chars, (char) c) > -1);
}
public static boolean isValueValid(String[] pair) {
  return !CONDITIONS.containsKey(pair[0])
       || CONDITIONS.get(pair[0]).test(pair[1]);
}

Ist das eine Java 9 Lösung? Gibt es auch eine Java 8 Lösung?

EDIT: Sorry, habs rausgefunden:

Java:
 private static final HashMap<String, Predicate<String>> CONDITIONS = new HashMap<String, Predicate<String>>(){
        {
            put("ws", maxLength(100));
            // usw
        };
   
};
 
Zuletzt bearbeitet:
EDIT: Sorry, habs rausgefunden:
Generell dazu: http://errorprone.info/bugpattern/DoubleBraceInitialization


Ist das eine Java 9 Lösung? Gibt es auch eine Java 8 Lösung?
Java:
private static final Map<String, Predicate<String>> CONDITIONS = createConditions();
private static createConditions() {
    final Map<String,Predicate<String>> conditions = new HashMap<>();
    conditions.put("ws", maxLength(100));
    conditions.put("xx", rangeLength(10, 20));
    conditions.put("yy", containsOnly('a', 'b', 'x')); // <- characters MUST be in ascending order!
    return conditions;
}

oder auch
Java:
private static final Map<String, Predicate<String>> CONDITIONS = new HashMap<>();
static {
    CONDITIONS.put("ws", maxLength(100));
    CONDITIONS.put("xx", rangeLength(10, 20));
    CONDITIONS.put("yy", containsOnly('a', 'b', 'x')); // <- characters MUST be in ascending order!
}
 

Zurück
Oben