Testing¶
Write and run tests for Fluvie
This guide covers how to write effective tests for Fluvie contributions.
Table of Contents¶
- Overview
- Test Types
- Running Tests
- Writing Widget Tests
- Gherkin Feature Tests
- Testing Animations
- Testing Templates
- Best Practices
Overview¶
Fluvie uses several testing approaches:
- Unit Tests: Test individual functions and classes
- Widget Tests: Test widget rendering and behavior
- Gherkin Tests: BDD-style feature tests
- Integration Tests: Test complete rendering pipeline
All tests should be deterministic and independent.
Test Types¶
Unit Tests¶
Test pure functions and simple classes:
// test/utils/interpolate_test.dart
void main() {
group('interpolate', () {
test('returns start value at frame 0', () {
final result = interpolate(
frame: 0,
inputRange: [0, 100],
outputRange: [0.0, 1.0],
);
expect(result, equals(0.0));
});
test('returns end value at final frame', () {
final result = interpolate(
frame: 100,
inputRange: [0, 100],
outputRange: [0.0, 1.0],
);
expect(result, equals(1.0));
});
test('interpolates midpoint correctly', () {
final result = interpolate(
frame: 50,
inputRange: [0, 100],
outputRange: [0.0, 1.0],
);
expect(result, closeTo(0.5, 0.001));
});
});
}
Widget Tests¶
Test widget rendering with frame control:
// test/presentation/time_consumer_test.dart
void main() {
testWidgets('TimeConsumer provides correct frame number', (tester) async {
int? capturedFrame;
await tester.pumpWidget(
MaterialApp(
home: RenderModeProvider(
frameNotifier: FrameReadyNotifier(42),
child: TimeConsumer(
builder: (context, frame, _) {
capturedFrame = frame;
return Text('Frame: $frame');
},
),
),
),
);
expect(capturedFrame, equals(42));
expect(find.text('Frame: 42'), findsOneWidget);
});
}
Integration Tests¶
Test the full rendering pipeline:
// test/integration/render_service_test.dart
void main() {
testWidgets('RenderService produces valid video', (tester) async {
final video = Video(
fps: 30,
width: 320,
height: 240,
scenes: [
Scene(
durationInFrames: 30,
children: [
Container(color: Colors.blue),
],
),
],
);
final outputPath = '${Directory.systemTemp.path}/test_output.mp4';
await RenderService.execute(
composition: video,
outputPath: outputPath,
tester: tester,
);
// Verify output exists
expect(File(outputPath).existsSync(), isTrue);
// Verify video properties
final info = await VideoProbeService.probe(outputPath);
expect(info.width, equals(320));
expect(info.height, equals(240));
expect(info.fps, closeTo(30, 1));
// Cleanup
File(outputPath).deleteSync();
});
}
Running Tests¶
All Tests¶
Specific Test File¶
Specific Test Group¶
With Verbose Output¶
With Coverage¶
Watch Mode¶
# Using flutter_test_watch (install separately)
flutter pub global activate flutter_test_watch
flutter_test_watch
Writing Widget Tests¶
Basic Widget Test¶
testWidgets('Scene renders children', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: RenderModeProvider(
frameNotifier: FrameReadyNotifier(0),
child: Scene(
durationInFrames: 60,
children: [
Container(
key: const Key('test-container'),
color: Colors.red,
),
],
),
),
),
);
expect(find.byKey(const Key('test-container')), findsOneWidget);
});
Testing Frame-Based Animation¶
testWidgets('Fade widget animates opacity', (tester) async {
final frameNotifier = FrameReadyNotifier(0);
await tester.pumpWidget(
MaterialApp(
home: RenderModeProvider(
frameNotifier: frameNotifier,
child: Fade(
startFrame: 0,
fadeInFrames: 30,
child: Container(color: Colors.blue),
),
),
),
);
// Frame 0: Should be invisible
expect(_getOpacity(tester), closeTo(0.0, 0.01));
// Frame 15: Should be 50% visible
frameNotifier.setFrame(15);
await tester.pump();
expect(_getOpacity(tester), closeTo(0.5, 0.01));
// Frame 30: Should be fully visible
frameNotifier.setFrame(30);
await tester.pump();
expect(_getOpacity(tester), closeTo(1.0, 0.01));
});
double _getOpacity(WidgetTester tester) {
final fadeTransition = tester.widget<FadeTransition>(
find.byType(FadeTransition),
);
return fadeTransition.opacity.value;
}
Testing with Mock Data¶
testWidgets('StatCard displays formatted value', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: RenderModeProvider(
frameNotifier: FrameReadyNotifier(100), // After animation
child: StatCard(
title: 'Plays',
value: 1000000,
startFrame: 0,
durationInFrames: 60,
),
),
),
);
expect(find.text('Plays'), findsOneWidget);
expect(find.text('1,000,000'), findsOneWidget); // Formatted
});
Gherkin Feature Tests¶
Fluvie uses Gherkin-style BDD tests for feature specifications.
Feature File¶
# test/features/video_composition.feature
Feature: Video Composition
Scenario: Basic video rendering
Given I define a VideoComposition with fps 30 and durationInFrames 90
When the RenderService executes the composition
Then the final video file duration is exactly 3 seconds
Scenario: Scene transitions
Given I define two consecutive timelines with a CrossFadeTransition lasting 15 frames
When the FFmpegFilterGraphBuilder processes the transition point
Then the output filtergraph uses an overlay filter with an enable and alpha expression
Step Definitions¶
// test/features/step/i_define_a_videocomposition.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:fluvie/fluvie.dart';
class VideoCompositionContext {
Video? video;
String? outputPath;
}
final context = VideoCompositionContext();
void iDefineAVideoCompositionWithFps30AndDurationInFrames90() {
context.video = Video(
fps: 30,
width: 1080,
height: 1920,
scenes: [
Scene(
durationInFrames: 90,
children: [
Container(color: Colors.blue),
],
),
],
);
}
void theRenderServiceExecutesTheComposition(WidgetTester tester) async {
context.outputPath = '${Directory.systemTemp.path}/test_${DateTime.now().millisecondsSinceEpoch}.mp4';
await RenderService.execute(
composition: context.video!,
outputPath: context.outputPath!,
tester: tester,
);
}
void theFinalVideoFileDurationIsExactly3Seconds() async {
final info = await VideoProbeService.probe(context.outputPath!);
expect(info.duration.inSeconds, equals(3));
}
Running Feature Tests¶
# Run all feature tests
flutter test test/features/
# Run specific feature
flutter test test/features/video_composition_test.dart
Testing Animations¶
PropAnimation Tests¶
void main() {
group('PropAnimation.slideUp', () {
test('starts at offset position', () {
final animation = PropAnimation.slideUp(distance: 100);
final transform = animation.transformAt(0.0);
expect(transform.getTranslation().y, equals(100.0));
});
test('ends at original position', () {
final animation = PropAnimation.slideUp(distance: 100);
final transform = animation.transformAt(1.0);
expect(transform.getTranslation().y, equals(0.0));
});
test('respects easing curve', () {
final animation = PropAnimation.slideUp(
distance: 100,
curve: Curves.easeInOut,
);
// At 50% progress with easeInOut, should be close to middle
final transform = animation.transformAt(0.5);
expect(transform.getTranslation().y, closeTo(50.0, 5.0));
});
});
}
Visual Animation Testing¶
testWidgets('AnimatedProp applies transform correctly', (tester) async {
final frameNotifier = FrameReadyNotifier(0);
await tester.pumpWidget(
MaterialApp(
home: RenderModeProvider(
frameNotifier: frameNotifier,
child: AnimatedProp(
startFrame: 0,
duration: 30,
animation: PropAnimation.scale(from: 0.0, to: 1.0),
child: Container(
key: const Key('target'),
width: 100,
height: 100,
color: Colors.red,
),
),
),
),
);
// Check transform at different frames
for (final frame in [0, 15, 30]) {
frameNotifier.setFrame(frame);
await tester.pump();
final transform = tester.widget<Transform>(
find.ancestor(
of: find.byKey(const Key('target')),
matching: find.byType(Transform),
),
);
final expectedScale = frame / 30;
final matrix = transform.transform;
expect(matrix.getMaxScaleOnAxis(), closeTo(expectedScale, 0.01));
}
});
Testing Templates¶
Template Rendering Test¶
testWidgets('IntroTemplate renders all elements', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: RenderModeProvider(
frameNotifier: FrameReadyNotifier(60), // Mid-animation
child: TheNeonGate(
data: IntroData(
title: 'Test Title',
subtitle: '2024',
),
theme: TemplateTheme.neon,
),
),
),
);
expect(find.text('Test Title'), findsOneWidget);
expect(find.text('2024'), findsOneWidget);
});
Template Theming Test¶
testWidgets('Template applies theme colors', (tester) async {
const testTheme = TemplateTheme(
colorPalette: ColorPalette(
primary: Color(0xFFFF0000),
secondary: Color(0xFF00FF00),
accent: Color(0xFF0000FF),
text: Colors.white,
background: Colors.black,
),
// ...
);
await tester.pumpWidget(
MaterialApp(
home: RenderModeProvider(
frameNotifier: FrameReadyNotifier(0),
child: TheNeonGate(
data: IntroData(title: 'Test'),
theme: testTheme,
),
),
),
);
// Find background container and verify color
final container = tester.widget<Container>(
find.byType(Container).first,
);
expect(container.color, equals(Colors.black));
});
Best Practices¶
1. Use Meaningful Names¶
// Good
test('interpolate returns midpoint at 50% progress', () { ... });
// Bad
test('test1', () { ... });
2. One Assertion Per Test (When Practical)¶
// Good - focused tests
test('Fade starts invisible', () {
expect(fade.opacityAt(0), equals(0.0));
});
test('Fade ends fully visible', () {
expect(fade.opacityAt(1), equals(1.0));
});
// Acceptable - related assertions
test('Fade transitions correctly', () {
expect(fade.opacityAt(0), equals(0.0));
expect(fade.opacityAt(0.5), closeTo(0.5, 0.01));
expect(fade.opacityAt(1), equals(1.0));
});
3. Test Edge Cases¶
group('FrameRange', () {
test('handles zero-length range', () { ... });
test('handles negative frames', () { ... });
test('handles frame at exact boundary', () { ... });
});
4. Clean Up Test Resources¶
testWidgets('render test', (tester) async {
final outputPath = '${Directory.systemTemp.path}/test.mp4';
try {
await RenderService.execute(
composition: video,
outputPath: outputPath,
tester: tester,
);
// Assertions...
} finally {
// Always clean up
if (File(outputPath).existsSync()) {
File(outputPath).deleteSync();
}
}
});
5. Use Test Fixtures¶
// test/fixtures/test_videos.dart
Video createSimpleTestVideo({int length = 30}) {
return Video(
fps: 30,
width: 320,
height: 240,
scenes: [
Scene(
durationInFrames: length,
children: [Container(color: Colors.blue)],
),
],
);
}
// In tests
test('renders simple video', () async {
final video = createSimpleTestVideo(length: 60);
// ...
});
Related¶
- Development Setup - Environment setup
- Code Style - Coding standards
- Contributing - Contribution overview