Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion labs/lab-1/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

<properties>
<java.version>21</java.version>
<mockito.version>5.5.0</mockito.version>
</properties>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import pragmatech.digital.workshops.lab1.domain.Book;
import pragmatech.digital.workshops.lab1.repository.BookRepository;
Expand All @@ -23,11 +24,13 @@ class MockitoDemoTest {
@Mock
private BookRepository bookRepository;

// public int count() -> int -> 0
// public Integer count() -> null

@InjectMocks
private BookService bookService;

@Test
@Disabled("Disabled to prevent test failure due to Sunday restriction in BookService")
void shouldReturnBookWhenFound() {
// Arrange
when(bookRepository.findByIsbn("1234")).thenReturn(Optional.empty());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import java.time.LocalDate;
import java.util.Optional;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import pragmatech.digital.workshops.lab1.domain.Book;
import pragmatech.digital.workshops.lab1.repository.BookRepository;
import pragmatech.digital.workshops.lab1.service.BookAlreadyExistsException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pragmatech.digital.workshops.lab1.service.BookAlreadyExistsException;
import pragmatech.digital.workshops.lab1.util.TimeProvider;

import static java.time.DayOfWeek.SATURDAY;
import static java.time.DayOfWeek.SUNDAY;

public class RefactoredBookService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import java.time.ZoneId;
import java.util.Optional;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand All @@ -25,7 +24,6 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static tools.jackson.databind.ext.javatime.deser.JSR310StringParsableDeserializer.ZONE_ID;

@ExtendWith(MockitoExtension.class)
class Solution1UnitTest {
Expand Down Expand Up @@ -53,7 +51,6 @@ void shouldThrowExceptionWhenBookWithIsbnAlreadyExists() {

@Test
@DisplayName("Should create a book when ISBN does not exist")
@Disabled("Disabled to prevent test failure due to Sunday restriction in BookService")
void shouldCreateBookWhenIsbnDoesNotExist() {
// Arrange
BookService cut = new BookService(bookRepository);
Expand Down Expand Up @@ -87,7 +84,8 @@ void shouldCreateBookWhenIsbnDoesNotExist() {
void shouldThrowExceptionWhenTryingToRegisterBookOnSunday() {
// Arrange
TimeProvider timeProvider = mock(TimeProvider.class);
when(timeProvider.getCurrentDate()).thenReturn(LocalDate.of(2026, 3, 1)); // A Sunday
when(timeProvider.getCurrentDate())
.thenReturn(LocalDate.of(2026, 3, 1)); // A Sunday

RefactoredBookService cut = new RefactoredBookService(bookRepository, timeProvider);
String isbn = "9780134685991";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,20 @@ void shouldReturnNotFoundWhenBookDoesNotExistById() throws Exception {
@Test
void shouldReturnCreatedWithLocationWhenCreatingBookWithValidData() throws Exception {
// Arrange
BookCreationRequest request = new BookCreationRequest(
"123-1234567890",
"Test Book",
"Test Author",
LocalDate.of(2020, 1, 1)
);

when(bookService.createBook(any(BookCreationRequest.class))).thenReturn(1L);

// Act & Assert
mockMvc.perform(post("/api/books")
.with(SecurityMockMvcRequestPostProcessors.user("user").roles("USER"))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.content("""
{
"isbn": "123-1234567890",
"title": "Test Book",
"author": "Test Author",
"publishedDate": "2024-01-01"
}
"""))
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/api/books/1")));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,20 @@ void shouldReturnUnauthorizedWhenNoAuthenticationIsProvided() throws Exception {
// Arrange - No authentication credentials provided

// Act & Assert
mockMvc.perform(delete("/api/books/1"))
mockMvc
.perform(delete("/api/books/1"))
.andExpect(status().isUnauthorized());

// Verify - Service should not be called
verify(bookService, times(0)).deleteBook(any());
}

@Test
@WithMockUser(roles = "USER")
@WithMockUser(roles = "DUKE")
@DisplayName("Should return 403 Forbidden when authenticated with insufficient privileges")
void shouldReturnForbiddenWhenAuthenticatedWithInsufficientPrivileges() throws Exception {
// Arrange - User with insufficient privileges (USER role)

// Act & Assert
mockMvc.perform(delete("/api/books/1"))
.andExpect(status().isForbidden());

// Verify - Service should not be called
verify(bookService, times(0)).deleteBook(any());
}

@Test
Expand All @@ -81,9 +76,6 @@ void shouldReturnNoContentWhenAuthenticatedAsAdminAndBookExists() throws Excepti
// Act & Assert
mockMvc.perform(delete("/api/books/1"))
.andExpect(status().isNoContent());

// Verify - Service should be called with the book ID
verify(bookService, times(1)).deleteBook(1L);
}

@Test
Expand All @@ -98,7 +90,7 @@ void shouldReturnNotFoundWhenAuthenticatedAsAdminButBookDoesntExist() throws Exc
.andExpect(status().isNotFound());

// Verify - Service should be called with the book ID
verify(bookService, times(1)).deleteBook(999L);
//verify(bookService, times(1)).deleteBook(999L);
}
}

Expand Down Expand Up @@ -129,9 +121,6 @@ void shouldReturnCreatedWhenValidBookDataIsProvided() throws Exception {
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(header().string("Location", Matchers.containsString("/api/books/1")));

// Verify - Service should be called
verify(bookService, times(1)).createBook(any());
}

@Test
Expand All @@ -142,7 +131,7 @@ void shouldReturnBadRequestWhenInvalidBookDataIsProvided() throws Exception {
String invalidBookJson = """
{
"isbn": "",
"title": "",
"title": "Test",
"author": "Test Author",
"publishedDate": "2025-01-01"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

// @EnableJpaAuditing -> keep this class clean and outsource configuration to a separate class
//@EnableJpaAuditing
@SpringBootApplication
public class Lab3Application {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ class BookRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
.withInitScript("init-postgres.sql");

@Autowired
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@
import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.postgresql.PostgreSQLContainer;
import pragmatech.digital.workshops.lab3.entity.Book;
import pragmatech.digital.workshops.lab3.entity.BookStatus;
import pragmatech.digital.workshops.lab3.repository.BookRepository;
Expand All @@ -30,15 +28,11 @@
*/
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class Solution1DataJpaTest {

@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:16-alpine")
.withInitScript("init-postgres.sql");

@Autowired
Expand Down
13 changes: 6 additions & 7 deletions labs/lab-3/src/test/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
spring:
datasource:
url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH
username: sa
password: password
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: none
properties:
hibernate:
# format_sql: true
# highlight_sql: true
format_sql: true
highlight_sql: true


12 changes: 6 additions & 6 deletions labs/lab-7/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

## Learning Objectives

- Configure JUnit 5 parallel test execution via `junit-platform.properties`
- Understand the difference between JUnit 5 thread-level parallelism and Maven Surefire `forkCount` process-level forking
- Configure JUnit 6 parallel test execution via `junit-platform.properties`
- Understand the difference between JUnit 6 thread-level parallelism and Maven Surefire `forkCount` process-level forking
- Identify and fix test isolation issues caused by shared database state
- Apply `@Transactional` and UUID-based unique data as complementary isolation strategies

## Key Concepts

### JUnit 5 Parallel Execution
### JUnit 6 Parallel Execution

JUnit 5 supports running tests in parallel at both the class level and the method level. Configuration lives in `src/test/resources/junit-platform.properties`.
JUnit 6 supports running tests in parallel at both the class level and the method level. Configuration lives in `src/test/resources/junit-platform.properties`.

There are two independent axes of parallelism:

Expand Down Expand Up @@ -71,12 +71,12 @@ src/test/resources/

### Exercise 1: Configure and Observe Parallel Test Execution

Understand JUnit 5 parallel execution configuration and observe its effect on thread allocation.
Understand JUnit 6 parallel execution configuration and observe its effect on thread allocation.

**Tasks:**
1. Open `src/test/resources/junit-platform.properties` and review the current settings
2. Open `Exercise1ParallelExecutionTest.java` — the test methods print the current thread name
3. Run `mvn test` and observe which threads each test class runs on in the console output
3. Run `mvn test` or within IntelliJ and observe which threads each test class runs on in the console output
4. Try different parallelism strategies by modifying `junit-platform.properties`:
- Classes concurrent, methods `same_thread` (current default)
- Both classes and methods `concurrent`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* <ol>
* <li>Check the junit-platform.properties file in src/test/resources</li>
* <li>Run all tests and observe parallel execution in the log output</li>
* <li>Consult <a href="https://docs.junit.org/6.0.3/writing-tests/parallel-execution.html">JUnit Docs</a></li>
* <li>Experiment with different parallelism strategies:
* <ul>
* <li>a) Classes concurrent, methods same_thread (default - current setting)</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
*
* <p>Tasks:
* <ol>
* <li>Identify tests that fail when run in parallel (shared database state)</li>
* <li>Apply isolation strategies:
* <ul>
* <li>a) Use @Transactional for automatic rollback after each test</li>
Expand Down
Binary file added slides/assets/ai-image.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added slides/assets/newsletter-signup-qr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added slides/assets/tsbad-cover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added slides/assets/why-test-software.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 44 additions & 3 deletions slides/lab-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ Philip Riecks - [PragmaTech GmbH](https://pragmatech.digital/) - [@rieckpil](htt

# Organization

- Hotel WiFi: `TBD` Password: `TBD`
- Slides & Code will be shared
- Sharing links during the workshop:
- Please interrupt me any time if you have questions or want to share your experience
- Workshop lab requirements
- Java 21
Expand Down Expand Up @@ -156,6 +154,28 @@ Notes:

---


![bg right:33%](assets/why-test-software.jpg)

# Why Test Software?

---


![bg right:33%](assets/ai-image.jpg)


## Automated Testing in the AI Era

- AI generates the code; you own the consequences.
- Co-pilots don’t carry pagers. You do.
- The faster the code is generated, the faster you need to prove it’s correct.
- AI is the accelerator; your test suite is the brakes. You can't drive fast without both.
- Mass-produced code without (the correct) mass-produced tests is just technical debt at scale.

---


### Common Testing Misconceptions

- Testing is only for finding bugs
Expand Down Expand Up @@ -232,7 +252,28 @@ Good tests don't just catch bugs - they give you **fast feedback** and **confide

---

## Maven Coordinates

```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
```

---

<!-- _class: section -->

Expand Down Expand Up @@ -740,7 +781,7 @@ public class TimingExtension implements BeforeTestExecutionCallback, AfterTestEx
- IDE of your choice (I can support you with IntelliJ IDEA)
- Java 21
- Docker Engine configured to run with Testcontainers
- Work locally or use GitHub Codespaces (120 hours/month free) as a fallback if you have trouble setting up your local environment
- Work locally or use GitHub Codespaces (120 hours/month free) as a fallback if you have trouble setting up your local environment (don't forget to remove the Codespace after the workshop to avoid running out of hours)
- Fore Codespaces, pick at least 4-Cores (16 GB RAM) and region `Europe West`


Expand Down
Loading