๐ Project: JSON Model Tests
๐ Lesson 10: Testing Your Code
โฑ 25-30 min
๐ Intermediate
๐ฏ Objective
Write comprehensive tests for JSON parsing models. Test with valid data, invalid data, missing fields, and edge cases to ensure robust data handling.
Requirements
- Test successful parsing of valid JSON
- Test that FormatException is thrown for invalid JSON
- Test optional fields (null vs present)
- Test round-trip (parse โ serialize โ reparse)
- Test nested object parsing
- Use test fixtures (sample JSON data)
// ============================================
// FILE: lib/models.dart (Models to test)
// ============================================
class Product {
final int id;
final String name;
final double price;
final String? description;
final List categories;
final Map? metadata;
Product({
required this.id,
required this.name,
required this.price,
this.description,
this.categories = const [],
this.metadata,
});
factory Product.fromJson(Map json) {
return switch (json) {
{
'id': final int id,
'name': final String name,
'price': final num price,
} => Product(
id: id,
name: name,
price: price.toDouble(),
description: json['description'] as String?,
categories: json['categories'] != null
? List.from(json['categories'] as List)
: [],
metadata: json['metadata'] as Map?,
),
_ => throw FormatException('Invalid product JSON: $json'),
};
}
Map toJson() => {
'id': id,
'name': name,
'price': price,
if (description != null) 'description': description,
'categories': categories,
if (metadata != null) 'metadata': metadata,
};
}
class Order {
final String orderId;
final List products;
final double totalAmount;
final String status;
Order({
required this.orderId,
required this.products,
required this.totalAmount,
required this.status,
});
factory Order.fromJson(Map json) {
return switch (json) {
{
'orderId': final String orderId,
'products': final List products,
'totalAmount': final num totalAmount,
'status': final String status,
} => Order(
orderId: orderId,
products: products
.map((p) => Product.fromJson(p as Map))
.toList(),
totalAmount: totalAmount.toDouble(),
status: status,
),
_ => throw FormatException('Invalid order JSON'),
};
}
}
// ============================================
// FILE: test/models_test.dart (Tests)
// ============================================
import 'package:test/test.dart';
import '../lib/models.dart';
void main() {
// Test fixtures
final validProductJson = {
'id': 1,
'name': 'Dart Programming Book',
'price': 29.99,
'description': 'A comprehensive guide to Dart',
'categories': ['books', 'programming'],
'metadata': {'isbn': '123-456-789', 'pages': 350},
};
final minimalProductJson = {
'id': 2,
'name': 'Simple Item',
'price': 9.99,
};
final validOrderJson = {
'orderId': 'ORD-001',
'products': [validProductJson, minimalProductJson],
'totalAmount': 39.98,
'status': 'pending',
};
group('Product model', () {
group('fromJson()', () {
test('parses valid JSON with all fields', () {
final product = Product.fromJson(validProductJson);
expect(product.id, equals(1));
expect(product.name, equals('Dart Programming Book'));
expect(product.price, equals(29.99));
expect(product.description, equals('A comprehensive guide to Dart'));
expect(product.categories, contains('books'));
expect(product.categories, hasLength(2));
expect(product.metadata, isNotNull);
expect(product.metadata!['isbn'], equals('123-456-789'));
});
test('parses minimal JSON with only required fields', () {
final product = Product.fromJson(minimalProductJson);
expect(product.id, equals(2));
expect(product.name, equals('Simple Item'));
expect(product.price, equals(9.99));
expect(product.description, isNull);
expect(product.categories, isEmpty);
expect(product.metadata, isNull);
});
test('throws FormatException for invalid JSON', () {
final invalidJson = {'name': 'Missing ID'};
expect(
() => Product.fromJson(invalidJson),
throwsA(isA()),
);
});
test('throws FormatException for wrong types', () {
final wrongType = {'id': 'not_a_number', 'name': 'Test', 'price': 10};
expect(
() => Product.fromJson(wrongType),
throwsA(isA()),
);
});
});
group('toJson()', () {
test('serializes back to JSON correctly', () {
final product = Product.fromJson(validProductJson);
final json = product.toJson();
expect(json['id'], equals(1));
expect(json['name'], equals('Dart Programming Book'));
expect(json['price'], equals(29.99));
});
test('round-trip: parse โ serialize โ reparse', () {
final original = Product.fromJson(validProductJson);
final json = original.toJson();
final reParsed = Product.fromJson(json);
expect(reParsed.id, equals(original.id));
expect(reParsed.name, equals(original.name));
expect(reParsed.price, equals(original.price));
expect(reParsed.description, equals(original.description));
});
});
});
group('Order model', () {
test('parses order with multiple products', () {
final order = Order.fromJson(validOrderJson);
expect(order.orderId, equals('ORD-001'));
expect(order.products, hasLength(2));
expect(order.totalAmount, equals(39.98));
expect(order.status, equals('pending'));
});
test('order products are correctly parsed', () {
final order = Order.fromJson(validOrderJson);
expect(order.products[0].name, equals('Dart Programming Book'));
expect(order.products[0].price, equals(29.99));
expect(order.products[1].name, equals('Simple Item'));
expect(order.products[1].price, equals(9.99));
});
test('totalAmount matches sum of product prices', () {
final order = Order.fromJson(validOrderJson);
final sum = order.products.fold(0.0, (total, p) => total + p.price);
expect(order.totalAmount, closeTo(sum, 0.01));
});
});
}