Skip to content

test: Write a generic jsonEquals check and deepToJson function #259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions test/api/model/events_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:test/scaffolding.dart';
import 'package:zulip/api/model/events.dart';

import '../../example_data.dart' as eg;
import '../../stdlib_checks.dart';
import 'events_checks.dart';
import 'model_checks.dart';

Expand Down
20 changes: 0 additions & 20 deletions test/api/model/model_checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@ import 'package:checks/checks.dart';
import 'package:zulip/api/model/model.dart';

extension MessageChecks on Subject<Message> {
Subject<Map<String, dynamic>> get toJson => has((e) => e.toJson(), 'toJson');

void jsonEquals(Message expected) {
final expectedJson = expected.toJson();
expectedJson['reactions'] = it()..isA<List<Reaction>>().jsonEquals(expected.reactions);
toJson.deepEquals(expectedJson);
}

Subject<String> get content => has((e) => e.content, 'content');
Subject<bool> get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage');
Subject<int?> get lastEditTimestamp => has((e) => e.lastEditTimestamp, 'lastEditTimestamp');
Expand All @@ -23,21 +15,9 @@ extension ReactionsChecks on Subject<List<Reaction>> {
void deepEquals(_) {
throw UnimplementedError('Tried to call [Subject<List<Reaction>>.deepEquals]. Use jsonEquals instead.');
}
Copy link
Collaborator

@chrisbobbe chrisbobbe Aug 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional to leave in this ReactionsChecks on Subject<List<Reaction>>, with its deepEquals implementation that throws UnimplementedError?

The error still does apply, I think. It's jsonEquals, not deepEquals, that actually looks deeply enough to be useful in a test, given that Reaction doesn't have an == override. So the error is likely to be helpful when you encounter it.

But if the error is needed, then probably the need won't be unique to List<Reaction>s, right? Does it look odd to you to have an extension on something as specific as Subject<List<Reaction>>?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this would be equally appropriate on List of any type that we know doesn't have a == that you would want in a test, and that has a toJson that you'd want instead.

It does look a bit odd having it on this one type. But it was already written, and could be useful, so I didn't feel like deleting it. I probably wouldn't go around trying to systematically add these for other types that could equally use them.


void jsonEquals(List<Reaction> expected) {
// (cast, to bypass this extension's deepEquals implementation, which throws)
// ignore: unnecessary_cast
(this as Subject<List>).deepEquals(expected.map((r) => it()..isA<Reaction>().jsonEquals(r)));
}
}

extension ReactionChecks on Subject<Reaction> {
Subject<Map<String, dynamic>> get toJson => has((r) => r.toJson(), 'toJson');

void jsonEquals(Reaction expected) {
toJson.deepEquals(expected.toJson());
}

Subject<String> get emojiName => has((r) => r.emojiName, 'emojiName');
Subject<String> get emojiCode => has((r) => r.emojiCode, 'emojiCode');
Subject<ReactionType> get reactionType => has((r) => r.reactionType, 'reactionType');
Expand Down
1 change: 0 additions & 1 deletion test/api/route/messages_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import 'package:zulip/model/narrow.dart';
import '../../example_data.dart' as eg;
import '../../stdlib_checks.dart';
import '../fake_api.dart';
import '../model/model_checks.dart';
import 'route_checks.dart';

void main() {
Expand Down
67 changes: 67 additions & 0 deletions test/stdlib_checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,73 @@ extension NullableMapChecks<K, V> on Subject<Map<K, V>?> {
}
}

/// Convert [object] to a pure JSON-like value.
///
/// The result is similar to `jsonDecode(jsonEncode(object))`, but without
/// passing through a serialized form.
///
/// All JSON atoms (numbers, booleans, null, and strings) are used directly.
/// All JSON containers (lists, and maps with string keys) are copied
/// as their elements are converted recursively.
/// For any other value, a dynamic call `.toJson()` is made and
/// should return either a JSON atom or a JSON container.
Object? deepToJson(Object? object) {
// Implementation is based on the recursion underlying [jsonEncode],
// at [_JsonStringifier.writeObject] in the stdlib's convert/json.dart .
// (We leave out the cycle-checking, for simplicity / out of laziness.)

var (result, success) = _deeplyConvertShallowJsonValue(object);
if (success) return result;

final Object? shallowlyConverted;
try {
shallowlyConverted = (object as dynamic).toJson();
} catch (e) {
throw JsonUnsupportedObjectError(object, cause: e);
}

(result, success) = _deeplyConvertShallowJsonValue(shallowlyConverted);
if (success) return result;
throw JsonUnsupportedObjectError(object);
}

(Object? result, bool success) _deeplyConvertShallowJsonValue(Object? object) {
final Object? result;
switch (object) {
case null || bool() || String() || num():
result = object;
case List():
result = object.map((x) => deepToJson(x)).toList();
case Map() when object.keys.every((k) => k is String):
result = object.map((k, v) => MapEntry(k, deepToJson(v)));
default:
return (null, false);
}
return (result, true);
}

extension JsonChecks on Subject<Object?> {
/// Expects that the value is deeply equal to [expected],
/// after calling [deepToJson] on both.
///
/// Deep equality is computed by [MapChecks.deepEquals]
/// or [IterableChecks.deepEquals].
void jsonEquals(Object? expected) {
final expectedJson = deepToJson(expected);
final actualJson = has((e) => deepToJson(e), 'deepToJson');
switch (expectedJson) {
case null || bool() || String() || num():
return actualJson.equals(expectedJson);
case List():
return actualJson.isA<List>().deepEquals(expectedJson);
case Map():
return actualJson.isA<Map>().deepEquals(expectedJson);
case _:
assert(false);
}
}
}

extension UriChecks on Subject<Uri> {
Subject<String> get asString => has((u) => u.toString(), 'toString'); // TODO(checks): what's a good convention for this?

Expand Down