| Layer | Framework | Location | Command |
|---|---|---|---|
| Backend (unit) | JUnit 5 + Mockito | src/test/java/ |
mvn test |
| Backend (integration) | Spring Boot Test + TestContainers | src/test/java/ |
mvn verify |
| Frontend | Flutter Test | src/main/app/test/ |
flutter test |
TestContainers automatically starts a real PostgreSQL instance for integration tests. Docker must be running on the development machine.
# All tests (unit + integration)
mvn test
# Tests only, no package
mvn test -pl .
# Skip tests (for fast build)
mvn clean package -DskipTests
# Run single test class
mvn test -Dtest=FeedServiceTest
# Run single test method
mvn test -Dtest=FeedServiceTest#shouldReturnFeedsForUser// src/test/java/com/github/lamarios/newsku/services/FeedServiceTest.java
@ExtendWith(MockitoExtension.class)
class FeedServiceTest {
@Mock
private FeedRepository feedRepository;
@Mock
private OpenaiService openaiService;
@InjectMocks
private FeedService feedService;
@Test
void shouldReturnFeedsForUser() {
// Arrange
String userId = "user-123";
Feed feed = new Feed();
feed.setId("feed-1");
feed.setUserId(userId);
when(feedRepository.findByUserId(userId)).thenReturn(List.of(feed));
// Act
List<Feed> result = feedService.getFeedsForUser(userId);
// Assert
assertThat(result).hasSize(1);
assertThat(result.get(0).getId()).isEqualTo("feed-1");
verify(feedRepository).findByUserId(userId);
}
@Test
void shouldThrowWhenFeedNotFound() {
// Arrange
when(feedRepository.findByIdAndUserId(anyString(), anyString()))
.thenReturn(Optional.empty());
// Act & Assert
assertThatThrownBy(() -> feedService.getFeed("user-1", "unknown-id"))
.isInstanceOf(FeedNotFoundException.class);
}
}// src/test/java/com/github/lamarios/newsku/integration/FeedControllerIT.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class FeedControllerIT {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:18-alpine")
.withDatabaseName("newsku_test")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldReturnEmptyFeedsForNewUser() {
// Generate JWT token for test user (or mock auth)
ResponseEntity<List> response = restTemplate.exchange(
"/api/feeds",
HttpMethod.GET,
new HttpEntity<>(authHeaders()),
List.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEmpty();
}
}@WebMvcTest(FeedController.class)
class FeedControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private FeedService feedService;
@Test
@WithMockUser(username = "user-123")
void getFeeds_shouldReturn200() throws Exception {
when(feedService.getFeedsForUser("user-123"))
.thenReturn(Collections.emptyList());
mockMvc.perform(get("/api/feeds"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$").isArray());
}
}cd src/main/app
# Run all tests
flutter test
# Single test file
flutter test test/feed_service_test.dart
# With coverage
flutter test --coverage// src/main/app/test/feed_item_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:newsku/feed/models/feed_item.dart';
void main() {
group('FeedItem', () {
test('fromJson parses correctly', () {
final json = {
'id': 'item-1',
'title': 'Test Article',
'imageUrl': null,
'importanceScore': 0.85,
'read': false,
};
final item = FeedItem.fromJson(json);
expect(item.id, equals('item-1'));
expect(item.title, equals('Test Article'));
expect(item.imageUrl, isNull);
expect(item.importanceScore, closeTo(0.85, 0.001));
expect(item.read, isFalse);
});
test('toJson serializes correctly', () {
final item = FeedItem(
id: 'item-1',
title: 'Test',
read: true,
);
final json = item.toJson();
expect(json['id'], equals('item-1'));
expect(json['read'], isTrue);
});
});
}// src/main/app/test/feed_bloc_test.dart
import 'package:bloc_test/bloc_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:flutter_test/flutter_test.dart';
class MockFeedService extends Mock implements FeedService {}
void main() {
late MockFeedService mockFeedService;
setUp(() {
mockFeedService = MockFeedService();
});
group('FeedBloc', () {
blocTest<FeedBloc, FeedState>(
'emits [FeedLoading, FeedLoaded] when LoadFeeds succeeds',
build: () {
when(() => mockFeedService.getFeeds())
.thenAnswer((_) async => [Feed(id: '1', title: 'Feed 1')]);
return FeedBloc(mockFeedService);
},
act: (bloc) => bloc.add(LoadFeeds()),
expect: () => [
isA<FeedLoading>(),
isA<FeedLoaded>(),
],
);
});
}// src/main/app/test/widgets/feed_tile_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('FeedTile displays title', (WidgetTester tester) async {
final feed = Feed(id: '1', title: 'My Test Feed');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: FeedTile(feed: feed),
),
),
);
expect(find.text('My Test Feed'), findsOneWidget);
});
}- Create file:
src/test/java/com/github/lamarios/newsku/services/<Name>ServiceTest.java - Use
@ExtendWith(MockitoExtension.class) - Mock dependencies with
@Mock - Instantiate service with
@InjectMocks
- Create file:
src/test/java/com/github/lamarios/newsku/integration/<Name>IT.java - Use
@SpringBootTest+@Testcontainers - Start PostgreSQL container via
@Container - Inject properties via
@DynamicPropertySource
- Create file:
src/main/app/test/<name>_test.dart - Import
flutter_test - Use
group()+test()/testWidgets()
- docs/entwicklung.md — Dev setup, build commands
- docs/api-patterns.md — What should be tested