All about...Tests

von

Michael Albrecht - Java CAT & PHP Pussy
team neusta GmbH

https://github.com/mike4git/kata.git

Agenda

  1. Grundlegendes
  2. Testabdeckung
  3. Test Tools
  4. Prozesse
    • TDD Kata
  5. [Mutation Testing]

"Wir testen nicht..."

Kapitel 1 der haltlosen Bemerkungen

Testen durch Ausprobieren

Vorteile:

  • User-oriented Handling

Nachteile:

  • keine Konsistenz
  • keine 100%ige Integrität
  • keine 100%ige Kontinuität
  • meist kein oder schlechtes Reporting der Ergebnisse

Testen durch Programmablauf

Vorteile:

  • Codierter Test, keine manuellen Fehleingaben
  • Lesbar durch Programmierer

Nachteile:

  • technologisch nicht überall möglich
  • Meist keine ausreichenden Prüfungen

Testen durch Debugging

Vorteile:

  • Blick in den Code

Nachteile:

  • technisch anspruchsvoll
  • meist zu tiefgehend
  • White-Box Testing
  • getrennt von der Sicht des Benutzers
  • kostet viel Zeit durch integrativen Ansatz

Testen durch Tests (...hinterher)

Vorteile:

  • Die richtige Technik für QS
  • Automatisierung möglich
  • Konsistente, integre Prüfung
  • Testabdeckung kann ermittelt werden

Nachteile:

  • Man testet den Produktivcode
    nicht die Spezifikation
  • Es passiert hinterher ...hoffentlich

Testen durch Logging

Vorteile:

  • Es wird geloggt.

Nachteile:

  • Wo kein Logging, auch kein Test
  • Log-Spam
  • "Wer zu viel misst, misst Mist."

Google & Automatisierung

Scope Bug fixing costs
Unit Test 5€
Integration Test 50€
System Test 500€
Customer Test 5000€

Eigenschaften eines Tests

  • Manuell / Automatisiert
  • Statisch / Dynamisch
  • Zielsetzung
    • Funktionstest
    • Performance, Last, Grenzwertanalyse,...
  • Scope
    • Unit Test
    • Integration Test
      • Partieller Integrationstest
    • System Test ("fully integrated")
    • Customer Test

Test Scope

Wie sieht ein guter Test aus?

2.Ableitung der QS

Teststruktur

Ein Test besteht aus vier Phasen:

  1. Test Fixture (Vorbereitung)
  2. Test Execution (Ausführung)
  3. Test Assertions &
    Verifications (Prüfung)
  4. Test Tear down
    (Aufräumarbeiten)

JUnit 4

...a simple testing framework

Empty Test

package de.mike.tests;

import org.junit.Test;
   
public class NameDerTestKlasse {
        
   @Test
   public void irgendeinBeliebigerMethodenname() {
   }
                    
}

Testfixture

...
@BeforeClass
public static void doBeforeAllTests() { ... }

@AfterClass
public static void doAfterAllTests() { ... }

@Before
public void doBeforeEachTest() { ... }

@After
public void doAfterEachTest() { ... }

@Test
public void test1() { ... }

@Test
public void test2() { ... }
...

Testfixture - Execution

@BeforeClass - method

@Before - method
 Test 1 wird ausgeführt.
@After - method

@Before - method
 Test 2 wird ausgeführt.
@After - method

@AfterClass - method

Testmethoden~ und Testklassenspezifische Vor- und Nachbereitungen

Weitervererbung der Test Fixture Methoden

Categories

@Category(de.mike.kata.Integrationtest.class)
public class MySpecialIntegrationTest {
   ...
}

In diesem Fall ist de.mike.kata.Integrationtest ein Marker Interface.

Mit Categories kann man Tests zu Gruppen zusammenfassen und diese Gruppen gezielt ausführen.

Simple Boolean Assertions

import org.junit.Test;
import static org.junit.Assert.assertTrue;
 
public class SimpleTest {
 
   @Test
   public void test() {
      assertTrue("Das weiß doch jedes Kind.", 1+1 == 2);
   }
 
}
  • assertTrue(condition) / assertFalse(condition)
  • assertEquals(expected, actual) / assertNotEquals(expected, actual)
  • assertSame(expected, actual)
  • assertNull(obj) / assertNotNull(obj)

HAMCREST Assertions

...
   @Test
   public void test() {
      assertThat(myObj, is(not(nullValue()));
   }
...

Code says: "Assert that my object is not a null value."

Bitte mit Meldung

...
   assertThat("Fehler", meinZuPruefenderWert, is(erwarteterWert));
...

Im Fehlerfall kann eine Meldung ausgegeben werden.

Old Assertions

@Test
public void testOldAssertions() {
  assertEquals(1, 1);
  assertNotEquals(1.2345f, 1.2345d, 0.0d);

  assertTrue(2 >> 1 == 2 - 1);
  assertFalse(11 * 13 == 144);
  Integer firstNumber = Integer.valueOf(5);
  Integer thirdNumber = Integer.valueOf(5);
  Integer secondNumber = new Integer(5);

  assertSame(firstNumber, thirdNumber);
  assertNotSame(firstNumber, secondNumber);
  String word = new String("Hallo");

  assertNotSame(word, "Hallo");
  assertEquals(word, "Hallo");
  assertSame("Hallo Welt!", "Hallo " + "Welt!");
  assertArrayEquals(new int[] { 1, 2 }, new int[] { 1, 2 });
  assertNotEquals(new int[] { 1, 4 }, new int[] { 1, 3 });

  assertNull(null);
  assertNotNull(new String());
}

New Assertions

@Test
public void testNewAssertions() {
   Integer four = Integer.valueOf(4);
   assertThat(four, is(new Integer(4)));
   assertThat(four, is(not(Integer.valueOf(5))));

   assertThat(four, lessThan(5));
   assertThat(four, greaterThanOrEqualTo(4));

   assertThat(four, isA(Integer.class));
}

Combinable Assertions

@Test
public void testCombinableMatchers() throws Exception {
   Integer four = Integer.valueOf(4);
   assertThat(four, either(is(4)).or(is(5)));
   assertThat(four, isOneOf(1, 2, 4));
}

Collection Assertions

@Test
public void testListMatchers() throws Exception {
   List numbers = new ArrayList<>();
   Integer four = Integer.valueOf(4);
   numbers.add(four);

   assertThat(numbers, hasItem(four));
   assertThat(numbers, not(hasItem(Integer.valueOf(5))));

   numbers.add(Integer.valueOf(5));

   assertThat(numbers, hasSize(2));

   assertThat(numbers, hasItems(5, 4));

   assertThat(numbers, contains(4, 5));
   assertThat(numbers, not(contains(5, 4)));
}

Bean Property Check

@Test
public void testJavaBeanProperty() throws Exception {
   MyBeanType myBean = new MyBeanType();
   assertThat(myBean, hasProperty("test"));
   assertThat(myBean, hasProperty("test", is("whatever")));
}

Runners

@RunWith(Suite.class)
public class MySuite { ... }

Der JUnit Runner ist die Startklasse für Tests.

In den gängigen IDEs gibt es bereits graphische Testrunner.

Um eigene oder andere Runner zu benutzen verwendet man die @RunWith Annotation

Third Party Runners

  • SpringJUnit4ClassRunner
  • MockitoJUnitRunner
  • PowerMockRunner

Alternative Runners

  • Suite
  • Parameterized
  • Categories

Suite - mehrere Tests gebündelt

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({
  LoginTest.class,
  LogoutTest.class,
  NavigationTest.class,
  UpdateTest.class
})
public class MyTestSuite {
  // leer lassen
}

Nachteile bei Runners

Es kann bei einem Test immer nur EIN Runner verwendet werden.

Alternativ gibt es die meisten Runner als Rule.

Ignoring Tests

@Test
@Ignore("Test wird zu Testzwecken ausgeblendet.")
public void testSame() {
    assertThat(1, is(1));
}

Feste Ausführreihenfolge

import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestMethodOrder {

    @Test
    public void testA() { System.out.println("first"); }
    @Test
    public void testB() { System.out.println("second"); }
    @Test
    public void testC() { System.out.println("third"); }
}

Theories in theory

Seien zwei natürliche Zahlen a und b gegeben.

  • Es gilt, dass a + b = b + a. (Kommutativgesetz)
  • Wenn a,b > 0,
    dann gilt: a + b > a und a + b > b

Wie setzt man so etwas in einem Test um?

Test Timeout

@Test(timeout=1000)
public void testWithTimeout() {
  ...
}

Dieser Test schlägt fehl, wenn er länger als
1 Sekunde (=1000 millisecs) braucht.

Datapoint(s)

Handelt es sich um einen festen Wert,
verwenden wir@DataPoint

Handelt es sich um mehrere Werte (Array),
verwenden wir @DataPoints

Die Zuordnung geschieht über den Parametertyp.

Theories

@RunWith(Theories.class)
public class TheoryTest {

   @DataPoints
   public static Integer[] ints = new Integer[] { -1, 0, 1, 2 };

   @Theory
   public void testCommutative(final Integer a, final Integer b) {
      assertTrue(a + b == b + a);
   }
}

Der Test wird für alle Kombination
{(-1,-1), (-1,0), ..., (2,2)} ausgeführt, also 16 mal.

Parameterized

@RunWith(Parameterized.class)
    public class LinesOfCodeCounterTest {
    private LinesOfCodeCounter counter;
    private String code2Test;
    private int numberOfCodeLines;
     
    public LinesOfCodeCounterTest(String code, 
                       int numberOfLines,
                       String comment) {
        code2Test = code;
        numberOfCodeLines = numberOfLines;
        counter = new LinesOfCodeCounter();
    }
 
    @Parameters(name = "Test case {index} - {2}")
    public static Iterable data1() {
       return Arrays.asList(new Object[][] {
          { "public void doIt() {\n}", 2, "No comments" },
          { "// Kommentar\npublic void doIt() {\nSystem.out.println(\"Hello world!\");\n}", 3, "Line comment" },
          { "/* block comment \n second line \n*/\npublic void ...", 1, "Block comment" }
          });
    }
 
    @Test
    public void testSimpleCodeLines() {
          assertThat(counter.count(code2Test), is(numberOfCodeLines));
    }
}

Parametrisierte Tests

Manchmal möchte man den gleichen Test mit unterschiedlichen Werten durchlaufen lassen.

In diesen Fällen spricht man von parametrisierten Tests.

JUnit sieht hierfür zwei Lösungen vor:

  • Parameterized
  • JUnitParams

JUnit Rules

since JUnit 4.8

JUnitParams

@RunWith(JUnitParamsRunner.class)
public class LinesOfCodeCounterTest {
  @Test
  @Parameters(
      { "public void doIt() {\n}, 2, No comments" ,
         "// Kommentar\npublic void doIt() {\nSystem.out.println(\"Hello world!\");\n}, 3, Line comment" ,
         "/* block comment \n second line \n*/\npublic void ..., 1, Block comment" 
       }
      )
   public void testSimpleCodeLines(String code2Test, int numberOfLines, String failureText) {
         assertThat(failureText, counter.count(code2Test), is(numberOfLines));
   }
}

Verwendung von Rules

public RuleType ruleVariable = new RuleType();

Rules

Mit Rules lassen sich - ähnlich zu Aspekten in AOP - die Ausführung von Tests vor, nach oder während der Ausführung steuern.

Es gibt 5 Standardrules in JUnit:

  • ExpectedException
  • TemporaryFolder
  • Timeout
  • ErrorCollector
  • TestName

Temporary Folder

public class TemporaryFolderWithRuleTest {

    @Rule
    public TemporaryFolder folder = new TemporaryFolder();

    @Test
    public void test() throws Exception {
        File file = folder.newFile("test.txt");
        assertTrue(file.exists());
    }
}

Test des Exception Handlings

Ich kenne (aktuell) 5 Arten, Exceptions, die geworfen werden können, zu testen:

  • try...catch...
  • expected
  • JUnit Rules
  • Assert
  • catch...exception

@Test(expected = ...)

@Test(expected = IllegalArgumentException.class)
public void testNoNegativeDigits() throws Exception {
      obj.callMethodWithWrongParams(1,2,-1);
}

Vorteil: Sehr einfach umzusetzen

Nachteil: Keine Prüfung des Exception Objekts möglich.

try... catch...

...
@Test
public void testNoNegativeDigits() throws Exception {
   try {
      obj.callMethodWithWrongParams(1,2,-1);
      fail("IllegalArgumentException should have been thrown.");
   } catch (IllegalArgumentException ex) {
      assertThat(ex.getMessage(), is("Only digits [1..9] are allowed."));
   }
   ...
}

Vorteil: Analoges Handling im Code

Nachteil: Keine Coverage Berechnung mit JaCoCo möglich.

CatchException ...

@Test
public void testNoNegativeDigits() throws Exception {
    catchException(obj).callMethodWithWrongParams(1,2,-1);
    assertThat(caughtException(),
          is(instanceOf(IllegalArgumentException.class)));
}

Vorteil: 100%ige Code Coverage mit JaCoCo

Nachteil: Extra Bibliothek.

Expected Exception

@Rule
public ExpectedException expectedException = ExpectedException.none();
 
@Test
public void testNoNegativeDigits() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("Only digits [1..9] are allowed.");
    obj.callMethodWithWrongParams(1,2,-1);
}

Vorteil: Sehr elegante Umsetzung ohne try...catch

Nachteil: Keine Coverage Berechnung mit JaCoCo möglich.

CatchException ...

@Test
public void testNoNegativeDigits() throws Exception {
    catchException(obj).callMethodWithWrongParams(1,2,-1);
    assertThat(caughtException(),
          is(instanceOf(IllegalArgumentException.class)));
}

Vorteil: 100%ige Code Coverage mit JaCoCo

Nachteil: Extra Bibliothek.

Phpunit

...a PHP testing framework

Requirements

composer require --dev phpunit/phpunit

Um auf phpunit zugreifen zu können, muss die Abhängigkeit per composer geladen werden:

{
   ...
   "autoload-dev": {
        "psr-4": {
            "Your\\Basic\\Namespace\\Tests\\": "tests/"
        }
    },
    ...
}

Des Weiteren empfiehlt sich der Eintrag für die Namespace Deklaration in Tests (in composer.json):

Empty Test

<?php declare(strict_types=1);

namespace Kata\Tests\Kata;

use PHPUnit\Framework\TestCase;

class MyClassToCheckTest extends TestCase
{
    public function test_check_regular_case(): void
    {
        // Test Fixture
        
        // Test Execution
        
        // Test Assertions & Verifications
    }
}

Annotated Test

<?php declare(strict_types=1);

namespace Kata\Tests\Kata;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class MyClassToCheckTest extends TestCase
{
    #[Test] 
    public function check_regular_case(): void
    {
        
    }
}

Test Fixture

class MyClassToCheckTest extends TestCase
{
    public static function setUpBeforeClass(): void {}
    
    protected function setUp(): void {}
    
    #[Test] 
    public function check_regular_case(): void {}
    
    protected function tearDown(): void {}
    
    public static function tearDownAfterClass(): void {}

}

Test Run

php vendor/bin/phpunit [several_options]

via Konsole:

via IDE:

Test Run Configuration

via Konsole:

Test Run Configuration

via IDE:

Assertions

Test Coverage

  • C0 - Coverage (Statement Coverage)
  • C1 - Coverage (Branch Coverage)
  • C2 - Coverage (Path Coverage)
  • C3 - Coverage (Condition Coverage)

Start

1

2

3

4

5

Stop

Test Coverage

  • C0 - Coverage (Statement Coverage)
  • C1 - Coverage (Branch Coverage)
  • C2 - Coverage (Path Coverage)
  • C3 - Coverage (Condition Coverage)

Anzahl offener
Bugs

Aufwand

Lokaler Support

  • Testframework
    • JUnit, Hamcrest
    • Mockito
    • Powermock
  • IDE-Support
    • Eclipse, IntelliJ
  • Buildmanagement
    • Maven
  • Testing Tools
    • Selenium, JMeter

Teamsupport

  • Build Server
    • Jenkins
    • Hudson
  • QS-Tools
    • Sonarqube

Prozesse

  • TDD      Test Driven Development
  • ATDD    Acceptance Test Driven Development
  • BDD      Behaviour Driven Development

TDD Basics

Test First

    „…cause test last does not work.“
Known interfaces
    TDD ist keine Entwurfstechnik
Keine Tests = kein Refactoring
   Einsturz des Kartenhaus
Software Entwicklung ist Gartenpflege
   "Unkraut wird nicht gesät, muss aber
   entfernt werden."
   Entropie Prinzip

 

TDD Rules

  • Es darf nur Produktivcode geschrieben werden, solange ein Test fehlschlägt.
     
  • Es darf nicht mehr in einem Test geschrieben werden, als fehlschlägt.
    Kompilierfehler sind fehlgeschlagene Tests.
     
  • Es ist nicht erlaubt, mehr Produktivcode zu schreiben, als für den fehlschlagenden Test notwendig ist.

     

Mocks & Co.

Mocks bedeutet auf deutsch Attrappe

  • Mocks werden verwendet, wo die Verwendung eines "echten" Objekts zu teuer wäre.

Mutation
Testing

Na?!
Wieviele waren es?

Bitte achten Sie auf die rechte Seite und zählen alle Code Änderungen

Prozess

Finde die Mutanten
...

und töte sie!!!

Powermock

Extension für EasyMock und Mockito

 

Support für JUnit und TestNG

Powermock Mockito JUnit
1.5.6 1.9.5 4.0 - 4.11
1.6.2 - 2.0 1.10.* >= 4.0
1.7.* 2.8.* >= 4.0

Kompatibilitätsmatrix

Configuration

<properties>
    <powermock.version>1.7.1</powermock.version>
</properties>
<dependencies>
   <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-module-junit4</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
   </dependency>
   <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-api-mockito</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
   </dependency>
</dependencies>

Um in Maven Powermock unter JUnit zu verwenden, braucht es folgende Dependency:

SimpleTest

@RunWith(PowerMockRunner.class)
@PrepareForTest( { YourClassWithStaticMethod.class })
public class YourTestCase {
   ...
}

Um einen einfachen Test mit Powermock durchzuführen, benötigt man folgendes Grundgerüst:

Internal State

@Test
public void testChangingInternalState() throws Exception {

   TypeWithReadonlyField sut = Whitebox.invokeConstructor(TypeWithReadonlyField.class);

   assertThat(sut.getValue(), is("internal"));
   Whitebox.setInternalState(sut, "value", "different");
   assertThat(sut.getValue(), is("different"));
}

Mit Hilfe der Klasse org.powermock.reflect.Whitebox lassen sich:

  • private Felder ändern und auslesen          
  • private Methoden aufrufen
  • private Konstruktor aufrufen

Suppression

// Beispiel für Methodenunterdrückung
@Test
public void testSuppressMethod() throws Exception {
   ...
   suppress(method(ExampleWithEvilMethod.class, "getEvilMessage"));
   ...
}

Mit Powermock lassen sich unterschiedliche Dinge unterdrücken, deren Ausführung zu Laufzeitproblemen in Tests kommen können:

  • (vererbte) Konstruktoren
  • Methoden
  • static Initialisierer
  • Felder

Mock System Classes

@RunWith(PowerMockRunner.class)
@PrepareForTest({ System.class })
public class MockSystemClassesTest {

   @Test
   public void assertThatMockingOfNonFinalSystemClassesWorks() {
      mockStatic(System.class);
      when(System.currentTimeMillis()).thenReturn(0L);
      assertThat(System.currentTimeMillis(), is(0L));
   }
}

Mock Static Methods (1/2)

package powermock;

public class ClassWithStaticMethods {

   public static int returnOne() {
      return 1;
   }

   public static int returnTwo() {
      return 2;
   }

   public static int returnThree() {
      return 3;
   }

}

Mock Static Methods (2/2)

package powermock;

import static org.hamcrest.CoreMatchers.is;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.junit.Assert.assertThat;

@RunWith(PowerMockRunner.class)
@PrepareForTest(ClassWithStaticMethods.class)
public class ClassCallingStaticMethodsTest {

   @Test
   public void testMethod() throws Exception {
      mockStatic(ClassWithStaticMethods.class);
      when(ClassWithStaticMethods.returnOne()).thenReturn(100);
      when(ClassWithStaticMethods.returnTwo()).thenReturn(200);
      when(ClassWithStaticMethods.returnThree()).thenReturn(300);

      assertThat(new ClassCallingStaticMethods().sumOfAllCalls(), is(600));
   }
}

All about...Tests

By Michael Albrecht

All about...Tests

  • 482