# Testrichtlinien
## Generelle Richtlinien
- Entwicklung nach TDD (Test-Driven-Development)
- Einhaltung des Prinzips der [Testpyramide](https://martinfowler.com/articles/practical-test-pyramid/testPyramid.png) (Viele low-level Tests, weniger bis wenige high level tests)
- Tests dürfen nicht mit dem Produktivsystem interagieren (Vorallem bei Integration Tests wichtig)
- Strukturierung für tests sollte immer sein:
1. Testdaten aufsetzen
2. Funktionalität mit Daten aus 1. Aufrufen
3. Überprüfen ob daten mit dem Soll übereinstimmen
### Unit tests
- Mocking von Dependencies die sich sehr negativ auf die Testlaufzeit auswirken würden
- Eine Test-Klasse pro Production Klasse
- Nach Beobachtbarem Verhalten Testen -> TDD
- *kein* testen von trivialem code
- Ein Test überprüft genau einen Umstand/Ausgang
### Vorgehensweise bei der Implementierung einer neuen Methode
**Triviale funktionalität, sowie frameworkfunktionalität also z.b. getter, setter sollen nicht getestet werden**
1. Erstellung der Interface Klasse und der Implementierungsklasse
2. Hinzufügen der neuen Methode zum Interface, mitsamt parametern und beschreibung (Was nimmt die methode, was gibt sie zurück)
3. Erstellung einer Testklasse
4. Erstellung mindestens eines, ggf. mehrerer Tests, die das von außen beobachtbare verhalten der Methode überprüfen *e.g. wird die methode mit a und b aufgerufen, ist das ergebnis dann c?* **nicht getestet werden soll** die implementierung, *e.g. wird die methode mit a und b aufgerufen, erfolgen dann aufrufe der methoden c und d*
4.1. Ein test sollte genau einen Umstand überprüfen (also z.b. keine tests die sowohl positive als auch negative umstände überprüfen)
4.2. Die Tests sollen den Umfang und das (von außen betrachtete) Verhalten der Methode abgrenzen und beschreiben -> hat eine Methode eine große Spanne von verschiedenen Rückgabewerten, so müssen dementsprechend viele tests geschrieben werden.
4.3. Beim ersten erstellen der Tests soll nicht an implementierungsdetails gedacht werden
5. Ausführen der Testfälle. Die neu erstellten Tests müssen fehlschlagen
6. Implementieren der Methode, sodass die Testfälle nicht mehr fehlschlagen
6.1. Werden in der Methode Dependencies verwendet welche hohen laufzeitaufwand haben, werden diese nachträglich in den tests gemockt
#### Beispiel
Beispiel eines Tests vor der Implementierung der Funktionalität
```java
public interface MoneyService {
/**
* Transfers the given amount from user1 to user2
* @param user1 The user from which the given amount shall be substracted
* @param user2 The user to which the given amount shall be added
* @return The Transferred amount
* @throws AccountOverdrawException if amount > user1.money
*/
BigDecimal transfer(User user1, User user2, BigDecimal amount) throws AccountOverdrawException;
}
@SpringBootTest
@Transactional
@ActiveProfiles("unittest")
public class MoneyServiceTest {
@Autowired
private MoneyService moneyService;
@Test
@DisplayName("Transferring from one user to the other transfers the correct amount")
public void transfer_user_otheruser_transfersCorrectAmount () {
//Setup Data
User user1 = new User("User1", new BigDecimal("1000.0"));
User user2 = new User("User2", new BigDecimal("100.0"));
//Execute Method
BigDecimal transferredAmount = this.moneyService.transfer(user1,
user2,
new BigDecimal("100.0"));
//Assert expected Results
assertEquals(transferredAmount, new BigDecimal("100.0"));
assertEquals(user1.getMoney(), new BigDecimal("900.0"));
assertEquals(user2.getMoney(), new BigDecimal("200.0"));
}
@Test
@DisplayName("Transferring more money than a user has throws an AccountOverdrawException")
public void transferTooMuchMoney_throws_AccountOverdrawException () {
//Setup Data
User user1 = new User("User1", new BigDecimal("100.0"));
User user2 = new User("User2", new BigDecimal("100.0"));
//Execute Method, Assert Results
assertThrows(AccountOverdrawException.class, () -> {
this.moneyService.transfer(user1, user2, new BigDecimal("150.0"))
});
}
}
```
Jetzt würde man die Methode implementieren. Würde während der Implementierung klar werden, dass man auf eine Laufzeitaufwendige schicht zugreifen muss (z.b. die Persistenz), so fügt man für diese nachträglich mocks zur Testklasse hinzu.
```java
/*...*/
@ExtendWith(MockitoExtension.class)
@Transactional
@MockitoSettings(strictness = Strictness.LENIENT)
@ActiveProfiles("test")
public class MoneyServiceTest {
@InjectMocks
private MoneyService moneyService;
@Mock
private AccountRepository accountRepository;
@Test
@DisplayName("Transferring from one user to the other transfers the correct amount")
public void transfer_user_otheruser_transfersCorrectAmount () {
//Setup Mocks
Mockito.when(accountRepository.substractMoney(user1, new BigDecimal("100.0")))
.thenReturn(new User("User1", new BigDecimal("900.0")));
Mockito.when(accountRepository.addMoney(user2, new BigDecimal("100.0")))
.thenReturn(new User("User2", new BigDecimal("200.0")));
//Setup Data
User user1 = new User("User1", new BigDecimal("1000.0"));
/*...*/
```
### Integration Tests
*Hier werde ich in den kommenden Tagen noch mehr ins Detail gehen*
- Sowohl Integration Tests mit kleinem als auch mit großem Umfang
- "kleine" Integration Tests testen die Interaktion mit externen Services/komponenten (Stripe, Osm)
- "große" Integration Tests testen die funktionalität der gesamten Applikation
### Frontend
- Tests für alle Services
- Tests für komplexere Components
## Ressourcen
[Mockito](https://www.baeldung.com/mockito-series)
[Aufbau von Unit Tests](https://martinfowler.com/articles/practical-test-pyramid.html#UnitTests)