Development Workflow
Using the Hypermodern CLI
The Hypermodern CLI is your primary development tool, providing project scaffolding, code generation, development servers, and deployment utilities. Mastering the CLI significantly improves your development velocity.
CLI Command Reference
# Project Management
hypermodern create <project_name> # Create new project
hypermodern create module <module_name> # Create new module
hypermodern init # Initialize existing directory
# Code Generation
hypermodern generate # Generate all code from schemas
hypermodern generate models # Generate only models
hypermodern generate client # Generate only client code
hypermodern generate server # Generate only server code
# Development Server
hypermodern serve # Start development server
hypermodern serve --http-port 8080 # Custom HTTP port
hypermodern serve --ws-port 8082 # Custom WebSocket port
hypermodern serve --tcp-port 8081 # Custom TCP port
hypermodern serve --no-watch # Disable hot reload
# Module Management
hypermodern module install <path_or_name> # Install module
hypermodern module uninstall <name> # Uninstall module
hypermodern module list # List installed modules
hypermodern module validate <path> # Validate module
# Schema Management
hypermodern schema validate # Validate schemas
hypermodern schema merge <files...> # Merge schema files
hypermodern schema diff <old> <new> # Compare schemas
# Database Operations
hypermodern db migrate # Run pending migrations
hypermodern db rollback <migration_id> # Rollback migration
hypermodern db status # Show migration status
hypermodern db seed # Run database seeders
# Production Build
hypermodern build # Build for production
hypermodern build --output dist # Custom output directory
hypermodern build --docker # Generate Docker files
# Testing
hypermodern test # Run all tests
hypermodern test --unit # Run unit tests only
hypermodern test --integration # Run integration tests only
hypermodern test --coverage # Generate coverage report
# Utilities
hypermodern lint # Lint code
hypermodern format # Format code
hypermodern analyze # Analyze code quality
hypermodern docs generate # Generate documentation
Advanced CLI Configuration
Create a hypermodern.yaml file in your project root for advanced configuration:
# hypermodern.yaml
project:
name: "my_hypermodern_app"
version: "1.0.0"
description: "My awesome Hypermodern application"
generation:
output_directory: "lib/generated"
separate_files: true
null_safety: true
immutable_models: true
generate_json_serialization: true
generate_binary_serialization: true
generate_equality: true
generate_copy_with: true
generate_to_string: true
server:
http_port: 8080
ws_port: 8082
tcp_port: 8081
bind_address: "0.0.0.0"
hot_reload: true
auto_restart: true
watch_patterns:
- "schemas/**/*.json"
- "lib/**/*.dart"
- "config/**/*.yaml"
database:
url: "postgresql://localhost:5432/myapp"
migrations_directory: "migrations"
auto_migrate: true
seed_directory: "seeds"
modules:
directory: "modules"
auto_install_dependencies: true
validate_on_install: true
build:
output_directory: "build"
minify: true
tree_shake: true
generate_source_maps: false
docker:
base_image: "dart:3.0-sdk"
expose_ports: [8080, 8082, 8081]
testing:
test_directory: "test"
coverage_directory: "coverage"
integration_test_timeout: "30s"
parallel_tests: true
linting:
rules_file: "analysis_options.yaml"
exclude_patterns:
- "lib/generated/**"
- "build/**"
documentation:
output_directory: "docs"
include_private: false
generate_examples: true
Custom CLI Commands
Extend the CLI with custom commands for your project:
// tools/custom_commands.dart
import 'package:hypermodern_cli/hypermodern_cli.dart';
class CustomCommands extends CommandGroup {
@override
String get name => 'custom';
@override
String get description => 'Custom project commands';
@override
List<Command> get commands => [
SetupDevEnvironmentCommand(),
GenerateTestDataCommand(),
DeployToStagingCommand(),
];
}
class SetupDevEnvironmentCommand extends Command {
@override
String get name => 'setup-dev';
@override
String get description => 'Set up development environment';
@override
Future<void> run() async {
print('Setting up development environment...');
// Create database
await _createDatabase();
// Run migrations
await _runMigrations();
// Seed test data
await _seedTestData();
// Install dependencies
await _installDependencies();
print('โ
Development environment ready!');
}
Future<void> _createDatabase() async {
print('Creating database...');
final result = await Process.run('createdb', ['myapp_dev']);
if (result.exitCode != 0) {
throw Exception('Failed to create database: ${result.stderr}');
}
}
Future<void> _runMigrations() async {
print('Running migrations...');
await HypermodernCLI.runCommand(['db', 'migrate']);
}
Future<void> _seedTestData() async {
print('Seeding test data...');
await HypermodernCLI.runCommand(['db', 'seed']);
}
Future<void> _installDependencies() async {
print('Installing dependencies...');
await Process.run('dart', ['pub', 'get']);
}
}
class GenerateTestDataCommand extends Command {
@override
String get name => 'generate-test-data';
@override
String get description => 'Generate test data for development';
@override
Future<void> run() async {
final count = int.tryParse(argResults?['count'] ?? '100') ?? 100;
print('Generating $count test records...');
final generator = TestDataGenerator();
await generator.generateUsers(count);
await generator.generatePosts(count * 3);
await generator.generateComments(count * 10);
print('โ
Test data generated successfully!');
}
@override
void defineOptions(ArgParser parser) {
parser.addOption('count', abbr: 'c', help: 'Number of records to generate');
}
}
Hot Reload and Development Server
The development server provides instant feedback through hot reload, automatic code generation, and multi-protocol testing capabilities.
Development Server Architecture
class DevelopmentServer {
final HypermodernServer _server;
final FileWatcher _fileWatcher;
final CodeGenerator _codeGenerator;
final MigrationRunner _migrationRunner;
final TestRunner _testRunner;
bool _isReloading = false;
final Set<String> _changedFiles = {};
Timer? _reloadTimer;
DevelopmentServer({
required HypermodernServer server,
required FileWatcher fileWatcher,
required CodeGenerator codeGenerator,
required MigrationRunner migrationRunner,
required TestRunner testRunner,
}) : _server = server,
_fileWatcher = fileWatcher,
_codeGenerator = codeGenerator,
_migrationRunner = migrationRunner,
_testRunner = testRunner;
Future<void> start() async {
print('๐ Starting Hypermodern development server...');
// Start the main server
await _server.listen();
// Set up file watching
await _setupFileWatching();
// Start additional development services
await _startDevelopmentServices();
print('โ
Development server ready!');
print(' HTTP: http://localhost:${_server.httpPort}');
print(' WebSocket: ws://localhost:${_server.wsPort}');
print(' TCP: localhost:${_server.tcpPort}');
print(' Admin UI: http://localhost:${_server.httpPort}/admin');
print('');
print('๐ Watching for changes...');
}
Future<void> _setupFileWatching() async {
// Watch schema files
_fileWatcher.watch('schemas/**/*.json', (event) {
_handleSchemaChange(event);
});
// Watch Dart source files
_fileWatcher.watch('lib/**/*.dart', (event) {
_handleSourceChange(event);
});
// Watch configuration files
_fileWatcher.watch('hypermodern.yaml', (event) {
_handleConfigChange(event);
});
// Watch migration files
_fileWatcher.watch('migrations/**/*.dart', (event) {
_handleMigrationChange(event);
});
}
void _handleSchemaChange(FileChangeEvent event) {
print('๐ Schema changed: ${event.path}');
_changedFiles.add(event.path);
_scheduleReload(ReloadType.codeGeneration);
}
void _handleSourceChange(FileChangeEvent event) {
// Skip generated files
if (event.path.contains('/generated/')) {
return;
}
print('๐ Source changed: ${event.path}');
_changedFiles.add(event.path);
_scheduleReload(ReloadType.hotReload);
}
void _handleConfigChange(FileChangeEvent event) {
print('โ๏ธ Configuration changed: ${event.path}');
_scheduleReload(ReloadType.fullRestart);
}
void _handleMigrationChange(FileChangeEvent event) {
print('๐๏ธ Migration changed: ${event.path}');
_scheduleReload(ReloadType.migration);
}
void _scheduleReload(ReloadType type) {
// Debounce rapid file changes
_reloadTimer?.cancel();
_reloadTimer = Timer(Duration(milliseconds: 500), () {
_performReload(type);
});
}
Future<void> _performReload(ReloadType type) async {
if (_isReloading) return;
_isReloading = true;
final stopwatch = Stopwatch()..start();
try {
switch (type) {
case ReloadType.codeGeneration:
await _performCodeGeneration();
break;
case ReloadType.hotReload:
await _performHotReload();
break;
case ReloadType.migration:
await _performMigration();
break;
case ReloadType.fullRestart:
await _performFullRestart();
break;
}
stopwatch.stop();
print('โ
Reload completed in ${stopwatch.elapsedMilliseconds}ms');
} catch (e) {
print('โ Reload failed: $e');
} finally {
_isReloading = false;
_changedFiles.clear();
}
}
Future<void> _performCodeGeneration() async {
print('๐ง Regenerating code...');
// Validate schemas first
final validation = await _codeGenerator.validateSchemas();
if (!validation.isValid) {
print('โ Schema validation failed:');
for (final error in validation.errors) {
print(' ${error.file}: ${error.message}');
}
return;
}
// Generate code
await _codeGenerator.generateAll();
// Hot reload the server
await _performHotReload();
}
Future<void> _performHotReload() async {
print('๐ฅ Hot reloading...');
// Reload server modules
await _server.reloadModules();
// Notify connected clients
await _notifyClientsOfReload();
// Run quick tests if enabled
if (_shouldRunTestsOnReload()) {
await _runQuickTests();
}
}
Future<void> _performMigration() async {
print('๐๏ธ Running migrations...');
await _migrationRunner.runPendingMigrations();
// Reload server to pick up schema changes
await _performHotReload();
}
Future<void> _performFullRestart() async {
print('๐ Performing full restart...');
// Stop current server
await _server.stop();
// Reload configuration
await _server.reloadConfiguration();
// Restart server
await _server.listen();
}
Future<void> _notifyClientsOfReload() async {
// Notify WebSocket clients
await _server.broadcastToWebSocketClients({
'type': 'hot_reload',
'timestamp': DateTime.now().toIso8601String(),
'changed_files': _changedFiles.toList(),
});
}
bool _shouldRunTestsOnReload() {
// Run tests if configuration enables it and changes are in source files
return _server.config.runTestsOnReload &&
_changedFiles.any((file) => file.endsWith('.dart'));
}
Future<void> _runQuickTests() async {
print('๐งช Running quick tests...');
final result = await _testRunner.runQuickTests();
if (result.success) {
print('โ
All tests passed');
} else {
print('โ ${result.failedTests} test(s) failed');
for (final failure in result.failures) {
print(' ${failure.testName}: ${failure.error}');
}
}
}
Future<void> _startDevelopmentServices() async {
// Start admin UI server
await _startAdminUI();
// Start API documentation server
await _startApiDocs();
// Start metrics collection
await _startMetricsCollection();
}
Future<void> _startAdminUI() async {
// Implementation for development admin UI
print('๐๏ธ Admin UI available at http://localhost:${_server.httpPort}/admin');
}
Future<void> _startApiDocs() async {
// Implementation for API documentation
print('๐ API docs available at http://localhost:${_server.httpPort}/docs');
}
Future<void> _startMetricsCollection() async {
// Implementation for development metrics
print('๐ Metrics available at http://localhost:${_server.httpPort}/metrics');
}
}
enum ReloadType {
codeGeneration,
hotReload,
migration,
fullRestart,
}
File Watching System
class FileWatcher {
final Map<String, StreamSubscription> _watchers = {};
final Map<String, List<void Function(FileChangeEvent)>> _callbacks = {};
Future<void> watch(String pattern, void Function(FileChangeEvent) callback) async {
_callbacks.putIfAbsent(pattern, () => []).add(callback);
if (!_watchers.containsKey(pattern)) {
await _startWatching(pattern);
}
}
Future<void> _startWatching(String pattern) async {
final glob = Glob(pattern);
final watcher = DirectoryWatcher('.');
final subscription = watcher.events.listen((event) {
if (glob.matches(event.path)) {
final changeEvent = FileChangeEvent(
path: event.path,
type: _mapEventType(event.type),
timestamp: DateTime.now(),
);
final callbacks = _callbacks[pattern] ?? [];
for (final callback in callbacks) {
try {
callback(changeEvent);
} catch (e) {
print('Error in file watcher callback: $e');
}
}
}
});
_watchers[pattern] = subscription;
}
FileChangeType _mapEventType(ChangeType type) {
switch (type) {
case ChangeType.ADD:
return FileChangeType.created;
case ChangeType.MODIFY:
return FileChangeType.modified;
case ChangeType.REMOVE:
return FileChangeType.deleted;
default:
return FileChangeType.modified;
}
}
Future<void> stop() async {
for (final subscription in _watchers.values) {
await subscription.cancel();
}
_watchers.clear();
_callbacks.clear();
}
}
class FileChangeEvent {
final String path;
final FileChangeType type;
final DateTime timestamp;
FileChangeEvent({
required this.path,
required this.type,
required this.timestamp,
});
}
enum FileChangeType {
created,
modified,
deleted,
}
Code Generation Workflow
Advanced Code Generator
class AdvancedCodeGenerator {
final SchemaLoader _schemaLoader;
final TemplateEngine _templateEngine;
final CodeFormatter _formatter;
final DependencyAnalyzer _dependencyAnalyzer;
AdvancedCodeGenerator({
required SchemaLoader schemaLoader,
required TemplateEngine templateEngine,
required CodeFormatter formatter,
required DependencyAnalyzer dependencyAnalyzer,
}) : _schemaLoader = schemaLoader,
_templateEngine = templateEngine,
_formatter = formatter,
_dependencyAnalyzer = dependencyAnalyzer;
Future<GenerationResult> generateAll() async {
final stopwatch = Stopwatch()..start();
try {
// Load and validate schemas
final schemas = await _schemaLoader.loadAll();
final validation = await validateSchemas(schemas);
if (!validation.isValid) {
return GenerationResult.failure(validation.errors);
}
// Analyze dependencies
final dependencyGraph = await _dependencyAnalyzer.analyze(schemas);
// Generate code in dependency order
final generatedFiles = <GeneratedFile>[];
for (final schema in dependencyGraph.topologicalSort()) {
final files = await _generateForSchema(schema);
generatedFiles.addAll(files);
}
// Format generated code
await _formatGeneratedFiles(generatedFiles);
// Write files to disk
await _writeGeneratedFiles(generatedFiles);
stopwatch.stop();
return GenerationResult.success(
generatedFiles: generatedFiles,
duration: stopwatch.elapsed,
);
} catch (e) {
return GenerationResult.failure([
GenerationError(
file: 'unknown',
message: 'Code generation failed: $e',
type: GenerationErrorType.internal,
),
]);
}
}
Future<List<GeneratedFile>> _generateForSchema(Schema schema) async {
final files = <GeneratedFile>[];
// Generate models
if (schema.models.isNotEmpty) {
final modelFiles = await _generateModels(schema);
files.addAll(modelFiles);
}
// Generate enums
if (schema.enums.isNotEmpty) {
final enumFiles = await _generateEnums(schema);
files.addAll(enumFiles);
}
// Generate client code
if (schema.endpoints.isNotEmpty) {
final clientFiles = await _generateClient(schema);
files.addAll(clientFiles);
}
// Generate server code
if (schema.endpoints.isNotEmpty) {
final serverFiles = await _generateServer(schema);
files.addAll(serverFiles);
}
return files;
}
Future<List<GeneratedFile>> _generateModels(Schema schema) async {
final files = <GeneratedFile>[];
for (final model in schema.models.values) {
final context = ModelGenerationContext(
model: model,
schema: schema,
config: _getGenerationConfig(),
);
final content = await _templateEngine.render('model.dart.template', context);
files.add(GeneratedFile(
path: 'lib/generated/models/${model.name.snakeCase}.dart',
content: content,
type: GeneratedFileType.model,
));
}
// Generate barrel file
final barrelContent = await _generateModelsBarrel(schema.models.values);
files.add(GeneratedFile(
path: 'lib/generated/models.dart',
content: barrelContent,
type: GeneratedFileType.barrel,
));
return files;
}
Future<String> _generateModelsBarrel(Iterable<ModelDefinition> models) async {
final buffer = StringBuffer();
buffer.writeln('// GENERATED CODE - DO NOT MODIFY BY HAND');
buffer.writeln('// Generated by Hypermodern CLI');
buffer.writeln('');
for (final model in models) {
buffer.writeln("export '${model.name.snakeCase}.dart';");
}
return buffer.toString();
}
Future<void> _formatGeneratedFiles(List<GeneratedFile> files) async {
for (final file in files) {
if (file.path.endsWith('.dart')) {
file.content = await _formatter.format(file.content);
}
}
}
Future<void> _writeGeneratedFiles(List<GeneratedFile> files) async {
for (final file in files) {
final fileObj = File(file.path);
await fileObj.parent.create(recursive: true);
await fileObj.writeAsString(file.content);
}
}
Future<SchemaValidationResult> validateSchemas(List<Schema> schemas) async {
final errors = <GenerationError>[];
for (final schema in schemas) {
// Validate model definitions
for (final model in schema.models.values) {
final modelErrors = await _validateModel(model, schema);
errors.addAll(modelErrors);
}
// Validate endpoint definitions
for (final endpoint in schema.endpoints.values) {
final endpointErrors = await _validateEndpoint(endpoint, schema);
errors.addAll(endpointErrors);
}
// Validate enum definitions
for (final enumDef in schema.enums.values) {
final enumErrors = await _validateEnum(enumDef);
errors.addAll(enumErrors);
}
}
return SchemaValidationResult(
isValid: errors.isEmpty,
errors: errors,
);
}
Future<List<GenerationError>> _validateModel(
ModelDefinition model,
Schema schema,
) async {
final errors = <GenerationError>[];
// Check for reserved field names
final reservedNames = ['hashCode', 'runtimeType', 'toString'];
for (final field in model.fields.values) {
if (reservedNames.contains(field.name)) {
errors.add(GenerationError(
file: schema.file,
message: 'Field name "${field.name}" is reserved in model "${model.name}"',
type: GenerationErrorType.validation,
));
}
}
// Validate field types
for (final field in model.fields.values) {
if (field.type.isReference) {
final referencedModel = schema.models[field.type.referenceName];
if (referencedModel == null) {
errors.add(GenerationError(
file: schema.file,
message: 'Unknown model reference "${field.type.referenceName}" in field "${field.name}"',
type: GenerationErrorType.validation,
));
}
}
}
return errors;
}
GenerationConfig _getGenerationConfig() {
return GenerationConfig(
nullSafety: true,
immutableModels: true,
generateJsonSerialization: true,
generateBinarySerialization: true,
generateEquality: true,
generateCopyWith: true,
generateToString: true,
);
}
}
class GenerationResult {
final bool success;
final List<GeneratedFile> generatedFiles;
final List<GenerationError> errors;
final Duration? duration;
GenerationResult({
required this.success,
this.generatedFiles = const [],
this.errors = const [],
this.duration,
});
factory GenerationResult.success({
required List<GeneratedFile> generatedFiles,
required Duration duration,
}) {
return GenerationResult(
success: true,
generatedFiles: generatedFiles,
duration: duration,
);
}
factory GenerationResult.failure(List<GenerationError> errors) {
return GenerationResult(
success: false,
errors: errors,
);
}
}
class GeneratedFile {
final String path;
String content;
final GeneratedFileType type;
GeneratedFile({
required this.path,
required this.content,
required this.type,
});
}
enum GeneratedFileType {
model,
enum,
client,
server,
barrel,
}
Template System
class TemplateEngine {
final Map<String, Template> _templates = {};
final TemplateLoader _loader;
TemplateEngine(this._loader);
Future<void> loadTemplates() async {
final templateFiles = await _loader.loadAll();
for (final file in templateFiles) {
final template = Template.parse(file.content);
_templates[file.name] = template;
}
}
Future<String> render(String templateName, dynamic context) async {
final template = _templates[templateName];
if (template == null) {
throw ArgumentError('Template not found: $templateName');
}
return template.render(context);
}
}
class Template {
final String _content;
final List<TemplateNode> _nodes;
Template._(this._content, this._nodes);
factory Template.parse(String content) {
final parser = TemplateParser(content);
final nodes = parser.parse();
return Template._(content, nodes);
}
String render(dynamic context) {
final buffer = StringBuffer();
for (final node in _nodes) {
buffer.write(node.render(context));
}
return buffer.toString();
}
}
// Example model template
const String modelTemplate = '''
// GENERATED CODE - DO NOT MODIFY BY HAND
// Generated by Hypermodern CLI
{{#imports}}
import '{{.}}';
{{/imports}}
{{#model.documentation}}
/// {{.}}
{{/model.documentation}}
class {{model.name}} {
{{#model.fields}}
{{#documentation}}
/// {{.}}
{{/documentation}}
final {{type.dartType}} {{name}};
{{/model.fields}}
const {{model.name}}({
{{#model.fields}}
{{#required}}required {{/required}}this.{{name}},
{{/model.fields}}
});
{{#config.generateJsonSerialization}}
Map<String, dynamic> toJson() => {
{{#model.fields}}
'{{jsonName}}': {{#type.isOptional}}{{name}}{{#type.needsConversion}}?.{{conversionMethod}}(){{/type.needsConversion}}{{/type.isOptional}}{{^type.isOptional}}{{name}}{{#type.needsConversion}}.{{conversionMethod}}(){{/type.needsConversion}}{{/type.isOptional}},
{{/model.fields}}
};
factory {{model.name}}.fromJson(Map<String, dynamic> json) => {{model.name}}(
{{#model.fields}}
{{name}}: {{#type.fromJsonExpression}}{{.}}{{/type.fromJsonExpression}},
{{/model.fields}}
);
{{/config.generateJsonSerialization}}
{{#config.generateBinarySerialization}}
Uint8List toBinary() {
final writer = BinaryWriter();
{{#model.fields}}
{{#type.binaryWriteMethod}}writer.{{.}}({{name}});{{/type.binaryWriteMethod}}
{{/model.fields}}
return writer.toBytes();
}
factory {{model.name}}.fromBinary(Uint8List data) {
final reader = BinaryReader(data);
return {{model.name}}(
{{#model.fields}}
{{name}}: {{#type.binaryReadMethod}}reader.{{.}}(){{/type.binaryReadMethod}},
{{/model.fields}}
);
}
{{/config.generateBinarySerialization}}
{{#config.generateEquality}}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is {{model.name}} &&
runtimeType == other.runtimeType &&
{{#model.fields}}
{{name}} == other.{{name}}{{^last}} &&{{/last}}
{{/model.fields}};
@override
int get hashCode => Object.hash(
{{#model.fields}}
{{name}},
{{/model.fields}}
);
{{/config.generateEquality}}
{{#config.generateToString}}
@override
String toString() => '{{model.name}}({{#model.fields}}{{name}}: ${{name}}{{^last}}, {{/last}}{{/model.fields}})';
{{/config.generateToString}}
{{#config.generateCopyWith}}
{{model.name}} copyWith({
{{#model.fields}}
{{type.dartType}}? {{name}},
{{/model.fields}}
}) => {{model.name}}(
{{#model.fields}}
{{name}}: {{name}} ?? this.{{name}},
{{/model.fields}}
);
{{/config.generateCopyWith}}
}
''';
Testing Strategies
Multi-Protocol Testing Framework
class HypermodernTestFramework {
final List<ProtocolTestClient> _clients;
final TestDatabase _testDatabase;
final MockDataGenerator _mockDataGenerator;
HypermodernTestFramework({
required List<ProtocolTestClient> clients,
required TestDatabase testDatabase,
required MockDataGenerator mockDataGenerator,
}) : _clients = clients,
_testDatabase = testDatabase,
_mockDataGenerator = mockDataGenerator;
Future<void> runEndpointTests(String endpoint, EndpointTestSuite testSuite) async {
for (final client in _clients) {
await _runEndpointTestsForProtocol(endpoint, testSuite, client);
}
}
Future<void> _runEndpointTestsForProtocol(
String endpoint,
EndpointTestSuite testSuite,
ProtocolTestClient client,
) async {
group('${endpoint} - ${client.protocol}', () {
setUp(() async {
await _testDatabase.reset();
await _mockDataGenerator.seedTestData();
await client.connect();
});
tearDown(() async {
await client.disconnect();
});
for (final testCase in testSuite.testCases) {
test(testCase.name, () async {
await _runTestCase(testCase, client);
});
}
});
}
Future<void> _runTestCase(EndpointTestCase testCase, ProtocolTestClient client) async {
try {
// Execute the test case
final result = await client.request(testCase.endpoint, testCase.request);
// Validate response
testCase.validateResponse(result);
// Validate side effects
if (testCase.sideEffectValidators.isNotEmpty) {
for (final validator in testCase.sideEffectValidators) {
await validator.validate(_testDatabase);
}
}
} catch (e) {
if (testCase.expectsError) {
testCase.validateError(e);
} else {
rethrow;
}
}
}
}
class EndpointTestSuite {
final String endpoint;
final List<EndpointTestCase> testCases;
EndpointTestSuite({
required this.endpoint,
required this.testCases,
});
}
class EndpointTestCase {
final String name;
final String endpoint;
final dynamic request;
final bool expectsError;
final void Function(dynamic) validateResponse;
final void Function(dynamic) validateError;
final List<SideEffectValidator> sideEffectValidators;
EndpointTestCase({
required this.name,
required this.endpoint,
required this.request,
this.expectsError = false,
required this.validateResponse,
this.validateError = _defaultErrorValidator,
this.sideEffectValidators = const [],
});
static void _defaultErrorValidator(dynamic error) {
// Default error validation - just ensure an error occurred
expect(error, isNotNull);
}
}
// Example test suite
class UserEndpointTests {
static EndpointTestSuite createTestSuite() {
return EndpointTestSuite(
endpoint: 'user_management',
testCases: [
EndpointTestCase(
name: 'should create user successfully',
endpoint: 'create_user',
request: CreateUserRequest(
username: 'testuser',
email: 'test@example.com',
password: 'securepassword123',
),
validateResponse: (response) {
expect(response, isA<User>());
final user = response as User;
expect(user.username, equals('testuser'));
expect(user.email, equals('test@example.com'));
expect(user.id, isPositive);
},
sideEffectValidators: [
DatabaseRecordValidator(
table: 'users',
expectedCount: 1,
conditions: {'username': 'testuser'},
),
],
),
EndpointTestCase(
name: 'should reject duplicate email',
endpoint: 'create_user',
request: CreateUserRequest(
username: 'testuser2',
email: 'existing@example.com', // This email already exists
password: 'securepassword123',
),
expectsError: true,
validateResponse: (_) => fail('Should have thrown an error'),
validateError: (error) {
expect(error, isA<EmailExistsException>());
},
),
EndpointTestCase(
name: 'should get user by id',
endpoint: 'get_user',
request: GetUserRequest(id: 1), // Assuming user with ID 1 exists
validateResponse: (response) {
expect(response, isA<User>());
final user = response as User;
expect(user.id, equals(1));
},
),
EndpointTestCase(
name: 'should return 404 for non-existent user',
endpoint: 'get_user',
request: GetUserRequest(id: 99999),
expectsError: true,
validateResponse: (_) => fail('Should have thrown an error'),
validateError: (error) {
expect(error, isA<NotFoundException>());
},
),
],
);
}
}
Integration Testing
class IntegrationTestRunner {
final HypermodernServer _server;
final List<HypermodernClient> _clients;
final TestDatabase _database;
IntegrationTestRunner({
required HypermodernServer server,
required List<HypermodernClient> clients,
required TestDatabase database,
}) : _server = server,
_clients = clients,
_database = database;
Future<void> runIntegrationTests() async {
group('Integration Tests', () {
setUpAll(() async {
await _database.initialize();
await _server.start();
for (final client in _clients) {
await client.connect();
}
});
tearDownAll(() async {
for (final client in _clients) {
await client.disconnect();
}
await _server.stop();
await _database.cleanup();
});
group('User Workflow', () {
test('complete user registration and login flow', () async {
await _testUserRegistrationFlow();
});
test('user profile management', () async {
await _testUserProfileFlow();
});
});
group('Real-time Features', () {
test('WebSocket notifications', () async {
await _testWebSocketNotifications();
});
test('TCP streaming', () async {
await _testTcpStreaming();
});
});
group('Cross-Protocol Consistency', () {
test('same data across all protocols', () async {
await _testCrossProtocolConsistency();
});
});
});
}
Future<void> _testUserRegistrationFlow() async {
final httpClient = _clients.firstWhere((c) => c.protocol == 'http');
// Register user
final registerRequest = RegisterRequest(
username: 'integrationtest',
email: 'integration@test.com',
password: 'testpassword123',
);
final registerResponse = await httpClient.request<RegisterResponse>(
'register',
registerRequest,
);
expect(registerResponse.user.username, equals('integrationtest'));
// Login with created user
final loginRequest = LoginRequest(
email: 'integration@test.com',
password: 'testpassword123',
);
final loginResponse = await httpClient.request<LoginResponse>(
'login',
loginRequest,
);
expect(loginResponse.accessToken, isNotEmpty);
expect(loginResponse.user.id, equals(registerResponse.user.id));
}
Future<void> _testWebSocketNotifications() async {
final wsClient = _clients.firstWhere((c) => c.protocol == 'websocket');
final httpClient = _clients.firstWhere((c) => c.protocol == 'http');
// Set up WebSocket listener
final notifications = <UserNotification>[];
final subscription = wsClient.stream<UserNotification>(
'user_notifications',
UserNotificationRequest(userId: 1),
).listen((notification) {
notifications.add(notification);
});
// Trigger notification via HTTP
await httpClient.request<void>(
'send_notification',
SendNotificationRequest(
userId: 1,
message: 'Test notification',
type: NotificationType.info,
),
);
// Wait for notification
await Future.delayed(Duration(seconds: 1));
expect(notifications, hasLength(1));
expect(notifications.first.message, equals('Test notification'));
await subscription.cancel();
}
Future<void> _testCrossProtocolConsistency() async {
// Create user via HTTP
final httpClient = _clients.firstWhere((c) => c.protocol == 'http');
final createRequest = CreateUserRequest(
username: 'crossprotocol',
email: 'cross@protocol.com',
password: 'password123',
);
final createdUser = await httpClient.request<User>('create_user', createRequest);
// Fetch user via WebSocket
final wsClient = _clients.firstWhere((c) => c.protocol == 'websocket');
final wsUser = await wsClient.request<User>(
'get_user',
GetUserRequest(id: createdUser.id),
);
// Fetch user via TCP
final tcpClient = _clients.firstWhere((c) => c.protocol == 'tcp');
final tcpUser = await tcpClient.request<User>(
'get_user',
GetUserRequest(id: createdUser.id),
);
// Verify consistency
expect(wsUser.id, equals(createdUser.id));
expect(wsUser.username, equals(createdUser.username));
expect(wsUser.email, equals(createdUser.email));
expect(tcpUser.id, equals(createdUser.id));
expect(tcpUser.username, equals(createdUser.username));
expect(tcpUser.email, equals(createdUser.email));
}
}
Performance Testing
class PerformanceTestRunner {
final List<HypermodernClient> _clients;
final TestDataGenerator _dataGenerator;
PerformanceTestRunner({
required List<HypermodernClient> clients,
required TestDataGenerator dataGenerator,
}) : _clients = clients,
_dataGenerator = dataGenerator;
Future<void> runPerformanceTests() async {
group('Performance Tests', () {
test('HTTP throughput test', () async {
await _testProtocolThroughput('http', iterations: 1000);
});
test('WebSocket throughput test', () async {
await _testProtocolThroughput('websocket', iterations: 1000);
});
test('TCP throughput test', () async {
await _testProtocolThroughput('tcp', iterations: 1000);
});
test('Load test with concurrent users', () async {
await _testConcurrentLoad(concurrency: 50, requestsPerUser: 100);
});
test('Memory usage under load', () async {
await _testMemoryUsage();
});
});
}
Future<void> _testProtocolThroughput(String protocol, {required int iterations}) async {
final client = _clients.firstWhere((c) => c.protocol == protocol);
await client.connect();
final request = GetUserRequest(id: 1);
final stopwatch = Stopwatch()..start();
for (int i = 0; i < iterations; i++) {
await client.request<User>('get_user', request);
}
stopwatch.stop();
final throughput = iterations / stopwatch.elapsed.inMilliseconds * 1000;
print('$protocol throughput: ${throughput.toStringAsFixed(1)} req/sec');
// Assert minimum performance requirements
expect(throughput, greaterThan(100)); // At least 100 req/sec
await client.disconnect();
}
Future<void> _testConcurrentLoad({required int concurrency, required int requestsPerUser}) async {
final futures = <Future>[];
for (int i = 0; i < concurrency; i++) {
final client = HypermodernClient('http://localhost:8080');
futures.add(_simulateUser(client, requestsPerUser));
}
final stopwatch = Stopwatch()..start();
await Future.wait(futures);
stopwatch.stop();
final totalRequests = concurrency * requestsPerUser;
final throughput = totalRequests / stopwatch.elapsed.inMilliseconds * 1000;
print('Concurrent load test: ${throughput.toStringAsFixed(1)} req/sec with $concurrency users');
// Assert performance under load
expect(throughput, greaterThan(500)); // At least 500 req/sec under load
}
Future<void> _simulateUser(HypermodernClient client, int requests) async {
await client.connect();
try {
for (int i = 0; i < requests; i++) {
// Simulate realistic user behavior
await client.request<User>('get_user', GetUserRequest(id: 1));
if (i % 10 == 0) {
// Occasionally create or update data
await client.request<User>('update_user', UpdateUserRequest(
id: 1,
username: 'user_${Random().nextInt(1000)}',
));
}
// Small delay to simulate user thinking time
await Future.delayed(Duration(milliseconds: 10));
}
} finally {
await client.disconnect();
}
}
Future<void> _testMemoryUsage() async {
final initialMemory = _getCurrentMemoryUsage();
// Generate load
await _testConcurrentLoad(concurrency: 20, requestsPerUser: 50);
// Force garbage collection
await _forceGarbageCollection();
final finalMemory = _getCurrentMemoryUsage();
final memoryIncrease = finalMemory - initialMemory;
print('Memory usage increase: ${_formatBytes(memoryIncrease)}');
// Assert memory usage is reasonable (less than 100MB increase)
expect(memoryIncrease, lessThan(100 * 1024 * 1024));
}
int _getCurrentMemoryUsage() {
// This would use platform-specific memory measurement
// Simplified for example
return ProcessInfo.currentRss;
}
Future<void> _forceGarbageCollection() async {
// Force multiple GC cycles
for (int i = 0; i < 5; i++) {
await Future.delayed(Duration(milliseconds: 100));
// System.gc() equivalent in Dart
}
}
String _formatBytes(int bytes) {
if (bytes < 1024) return '${bytes}B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB';
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB';
}
}
What's Next
You now have a comprehensive understanding of the Hypermodern development workflow, including CLI usage, hot reload capabilities, code generation, and testing strategies. The next chapter will cover production deployment, showing you how to build, containerize, and deploy Hypermodern applications to various environments with proper monitoring and scaling considerations.