Skip to main content
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

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

1

Run all tests

cd appflowy_flutter
flutter test
2

Run specific test file

flutter test test/user/user_profile_test.dart
3

Run tests with coverage

flutter test --coverage
View coverage report:
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
4

Run integration tests

flutter test integration_test

Using cargo-make for Dart Tests

AppFlowy provides convenient cargo-make tasks:
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

1

Run all tests

cd rust-lib
cargo test
2

Run specific module tests

cargo test --package flowy-user
3

Run tests with output

cargo test -- --nocapture
4

Run single test

cargo test test_user_profile_creation

Using cargo-make for Rust Tests

cd frontend
cargo make rust_unit_test

Test Backend

AppFlowy provides a test backend for Flutter tests:
1

Build test backend

cd frontend
cargo make build_test_backend
This builds the Rust backend with test-specific configuration.
2

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

1

Generate coverage

cd appflowy_flutter
flutter test --coverage
2

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

1

Install grcov

cargo install grcov
rustup component add llvm-tools-preview
2

Run tests with coverage

cd frontend
cargo make rust_unit_test_with_coverage
3

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

flutter clean
flutter pub get
flutter test

Rust Tests Fail

cargo clean
cargo test

Next Steps

Contributing

Contribute your changes with tests

Code Style

Follow coding conventions

Building

Build AppFlowy from source

Architecture

Understand the architecture

Build docs developers (and LLMs) love