Testing is crucial for maintaining code quality and preventing regressions. This guide covers testing practices for both Flutter and Rust components.
Testing Philosophy
AppFlowy follows a comprehensive testing strategy:
Unit Tests Test individual functions and classes in isolation
Widget Tests Test Flutter widgets and UI components
Integration Tests Test complete user flows and features
Rust Tests Test Rust backend logic and modules
Flutter Testing
Unit Tests
Unit tests verify individual functions and business logic:
// test/user/user_profile_test.dart
import 'package:flutter_test/flutter_test.dart' ;
import 'package:appflowy/user/domain/user_profile.dart' ;
void main () {
group ( 'UserProfile' , () {
test ( 'creates profile with valid data' , () {
final profile = UserProfile (
id : '123' ,
name : 'John Doe' ,
email : '[email protected] ' ,
);
expect (profile.id, equals ( '123' ));
expect (profile.name, equals ( 'John Doe' ));
expect (profile.email, equals ( '[email protected] ' ));
});
test ( 'validates email format' , () {
expect ( UserProfile . isValidEmail ( '[email protected] ' ), isTrue);
expect ( UserProfile . isValidEmail ( 'invalid' ), isFalse);
});
});
}
Widget tests verify UI components:
// test/widget/document_title_test.dart
import 'package:flutter/material.dart' ;
import 'package:flutter_test/flutter_test.dart' ;
import 'package:appflowy/workspace/presentation/widgets/document_title.dart' ;
void main () {
testWidgets ( 'DocumentTitle displays title' , (tester) async {
await tester. pumpWidget (
MaterialApp (
home : Scaffold (
body : DocumentTitle (title : 'My Document' ),
),
),
);
expect (find. text ( 'My Document' ), findsOneWidget);
});
testWidgets ( 'DocumentTitle handles tap' , (tester) async {
var tapped = false ;
await tester. pumpWidget (
MaterialApp (
home : Scaffold (
body : DocumentTitle (
title : 'My Document' ,
onTap : () => tapped = true ,
),
),
),
);
await tester. tap (find. byType ( DocumentTitle ));
expect (tapped, isTrue);
});
}
BLoC Tests
Test state management with BLoCs:
// test/bloc/document_bloc_test.dart
import 'package:flutter_test/flutter_test.dart' ;
import 'package:bloc_test/bloc_test.dart' ;
import 'package:appflowy/workspace/application/document/document_bloc.dart' ;
void main () {
group ( 'DocumentBloc' , () {
late DocumentBloc bloc;
setUp (() {
bloc = DocumentBloc (documentId : '123' );
});
tearDown (() {
bloc. close ();
});
blocTest < DocumentBloc , DocumentState >(
'emits loading and loaded states' ,
build : () => bloc,
act : (bloc) => bloc. add ( DocumentEvent . load ()),
expect : () => [
DocumentState . loading (),
DocumentState . loaded (document : mockDocument),
],
);
blocTest < DocumentBloc , DocumentState >(
'handles load error' ,
build : () => bloc,
act : (bloc) => bloc. add ( DocumentEvent . load ()),
expect : () => [
DocumentState . loading (),
DocumentState . error (message : 'Document not found' ),
],
);
});
}
Integration Tests
Integration tests verify complete user flows:
// integration_test/document_flow_test.dart
import 'package:flutter_test/flutter_test.dart' ;
import 'package:integration_test/integration_test.dart' ;
import 'package:appflowy/main.dart' as app;
void main () {
IntegrationTestWidgetsFlutterBinding . ensureInitialized ();
group ( 'Document Flow' , () {
testWidgets ( 'create and edit document' , (tester) async {
app. main ();
await tester. pumpAndSettle ();
// Tap create button
await tester. tap (find. byIcon ( Icons .add));
await tester. pumpAndSettle ();
// Enter document title
await tester. enterText (
find. byType ( TextField ),
'Test Document' ,
);
await tester. pumpAndSettle ();
// Verify document created
expect (find. text ( 'Test Document' ), findsOneWidget);
});
});
}
Running Flutter Tests
Run all tests
cd appflowy_flutter
flutter test
Run specific test file
flutter test test/user/user_profile_test.dart
Run tests with coverage
View coverage report: genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
Run integration tests
flutter test integration_test
Using cargo-make for Dart Tests
AppFlowy provides convenient cargo-make tasks:
Run Dart Unit Tests
Run Single Test
Run Tests Without Building
cd frontend
cargo make dart_unit_test
Rust Testing
Unit Tests
Rust unit tests are written inline with the code:
// rust-lib/flowy-user/src/user_profile.rs
pub struct UserProfile {
pub id : String ,
pub name : String ,
pub email : String ,
}
impl UserProfile {
pub fn new ( id : String , name : String , email : String ) -> Self {
Self { id , name , email }
}
pub fn is_valid_email ( email : & str ) -> bool {
email . contains ( '@' ) && email . contains ( '.' )
}
}
#[cfg(test)]
mod tests {
use super ::* ;
#[test]
fn test_user_profile_creation () {
let profile = UserProfile :: new (
"123" . to_string (),
"John Doe" . to_string (),
"[email protected] " . to_string (),
);
assert_eq! ( profile . id, "123" );
assert_eq! ( profile . name, "John Doe" );
}
#[test]
fn test_email_validation () {
assert! ( UserProfile :: is_valid_email ( "[email protected] " ));
assert! ( ! UserProfile :: is_valid_email ( "invalid" ));
}
}
Async Tests
Test async functions with tokio::test:
use tokio :: time :: {sleep, Duration };
pub async fn load_document ( id : & str ) -> Result < Document > {
sleep ( Duration :: from_millis ( 100 )) . await ;
// Load document...
Ok ( Document :: default ())
}
#[cfg(test)]
mod tests {
use super ::* ;
#[tokio :: test]
async fn test_load_document () {
let doc = load_document ( "123" ) . await . unwrap ();
assert! ( ! doc . id . is_empty ());
}
#[tokio :: test]
async fn test_concurrent_loads () {
let handles : Vec < _ > = ( 0 .. 10 )
. map ( | i | tokio :: spawn ( load_document ( & format! ( "{}" , i ))))
. collect ();
for handle in handles {
assert! ( handle . await . is_ok ());
}
}
}
Integration Tests
Integration tests are in the tests/ directory:
// rust-lib/flowy-user/tests/user_integration_test.rs
use flowy_user :: UserManager ;
#[tokio :: test]
async fn test_user_signup_flow () {
let manager = UserManager :: new ();
// Sign up new user
let result = manager . sign_up (
"[email protected] " ,
"password123" ,
"Test User" ,
) . await ;
assert! ( result . is_ok ());
// Verify user created
let user = manager . get_current_user () . await . unwrap ();
assert_eq! ( user . email, "[email protected] " );
}
Running Rust Tests
Run specific module tests
cargo test --package flowy-user
Run tests with output
cargo test -- --nocapture
Run single test
cargo test test_user_profile_creation
Using cargo-make for Rust Tests
Run Rust Unit Tests
Run with Coverage
Run Cloud Tests
cd frontend
cargo make rust_unit_test
Test Backend
AppFlowy provides a test backend for Flutter tests:
Build test backend
cd frontend
cargo make build_test_backend
This builds the Rust backend with test-specific configuration.
Run tests
The test backend is automatically loaded when running Dart tests.
The test backend is built as a dynamic library (cdylib) for easier loading in tests.
Mocking and Test Doubles
Dart Mocking
Use mockito for creating mocks:
import 'package:mockito/mockito.dart' ;
import 'package:mockito/annotations.dart' ;
@GenerateMocks ([ DocumentRepository ])
import 'document_test.mocks.dart' ;
void main () {
test ( 'loads document from repository' , () async {
final repository = MockDocumentRepository ();
when (repository. getDocument ( '123' ))
. thenAnswer ((_) async => mockDocument);
final service = DocumentService (repository);
final doc = await service. load ( '123' );
expect (doc.id, equals ( '123' ));
verify (repository. getDocument ( '123' )). called ( 1 );
});
}
Rust Mocking
Use traits for dependency injection:
#[async_trait]
pub trait DocumentRepository {
async fn get_document ( & self , id : & str ) -> Result < Document >;
}
pub struct RealDocumentRepository ;
#[async_trait]
impl DocumentRepository for RealDocumentRepository {
async fn get_document ( & self , id : & str ) -> Result < Document > {
// Real implementation
}
}
#[cfg(test)]
mod tests {
use super ::* ;
struct MockDocumentRepository ;
#[async_trait]
impl DocumentRepository for MockDocumentRepository {
async fn get_document ( & self , id : & str ) -> Result < Document > {
Ok ( Document {
id : id . to_string (),
.. Default :: default ()
})
}
}
#[tokio :: test]
async fn test_with_mock () {
let repo = MockDocumentRepository ;
let doc = repo . get_document ( "123" ) . await . unwrap ();
assert_eq! ( doc . id, "123" );
}
}
Test Coverage
Flutter Coverage
Generate coverage
cd appflowy_flutter
flutter test --coverage
View HTML report
# Install lcov (macOS)
brew install lcov
# Generate HTML
genhtml coverage/lcov.info -o coverage/html
# Open report
open coverage/html/index.html
Rust Coverage
Install grcov
cargo install grcov
rustup component add llvm-tools-preview
Run tests with coverage
cd frontend
cargo make rust_unit_test_with_coverage
View report
open rust-lib/target/coverage.lcov
CI/CD Testing
AppFlowy runs automated tests on all pull requests:
GitHub Actions Workflow
name : Tests
on :
pull_request :
push :
branches : [ main ]
jobs :
flutter-tests :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v3
- uses : subosito/flutter-action@v2
- run : flutter test
rust-tests :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v3
- uses : dtolnay/rust-toolchain@stable
- run : cargo test --workspace
Required Checks
All PRs must pass:
✅ Dart unit tests
✅ Rust unit tests
✅ Integration tests
✅ Code formatting
✅ Linting
PRs with failing tests will not be merged.
Best Practices
Write Tests First Consider TDD: write tests before implementing features
Test Behavior Test what the code does, not how it does it
Keep Tests Fast Fast tests = frequent testing = better quality
Isolate Tests Each test should be independent and repeatable
Use Descriptive Names Test names should explain what is being tested
Arrange-Act-Assert Structure tests clearly: setup, execute, verify
Test Structure
Follow the AAA pattern:
test ( 'should return user profile when ID is valid' , () {
// Arrange
final repository = MockUserRepository ();
final service = UserService (repository);
when (repository. getUser ( '123' )). thenReturn (mockUser);
// Act
final result = service. getProfile ( '123' );
// Assert
expect (result.id, equals ( '123' ));
expect (result.name, isNotEmpty);
});
Troubleshooting
Flutter Tests Fail
Clean and retry
Update dependencies
Run single test
flutter clean
flutter pub get
flutter test
flutter pub upgrade
flutter test
flutter test test/path/to/test.dart
Rust Tests Fail
Clean and retry
Update dependencies
Run with backtrace
RUST_BACKTRACE = 1 cargo test
Next Steps
Contributing Contribute your changes with tests
Code Style Follow coding conventions
Building Build AppFlowy from source
Architecture Understand the architecture