Skip to content

Commit

Permalink
feat: add charlatanResponse helper, add type-safety (#25)
Browse files Browse the repository at this point in the history
* feat! add charlatanResponse helper, add type-safety

fixes #24

this changeset adds a new `charlatanResponse` helper function that
returns a `CharlatanResponseBuilder` with a default status code.

this new helper function replaces the previous approach of returning an
`Object?` from the `CharlatanResponseBuilder` type. This makes it safer
to work with in your codebase but is a breaking change.

To migrate to this version, replace any usages of directly returning
anything other than `CharlatanHttpResponse` in `when*` methods with a
a call to `charlatanResponse` or an explicit `CharlatanHttpResponse`
instance.

* README updates

* recalc the semantic PR title check

* fix typo and add explicit future checking!
  • Loading branch information
samandmoore authored Apr 13, 2023
1 parent 0fad2e8 commit 671566f
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 93 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 0.4.0

- Add `charlatanResponse` helper to concisely create `CharlatanResponseBuilder` values
- Remove `statusCode` from `when*` methods
- Change type of `CharlatanResponseBuilder`
from `FutureOr<Object?> Function(CharlatanHttpRequest request, { int statusCode = 200 })`
to `FutureOr<CharlatanHttpResponse> Function(CharlatanHttpRequest request)`

## 0.3.1

- Export `CharlatanHttpRequest` and `CharlatanRequestMatcher`
Expand Down
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ responses from your [Dio HTTP Client](https://pub.dev/packages/dio).
This makes it easy to test the behavior of code that interacts with
HTTP services without having to use mocks.

It consists of two components:
It consists of two components and a few helper functions:

- `Charlatan` - a class for configuring and providing fake HTTP responses
based on HTTP method and URI template.
- `CharlatanHttpClientAdapter` - an implementation of Dio's
`HttpClientAdapter` that returns responses from a configured
`Charlatan` instance.
- `charlatanResponse` and request matching helpers - utilites for concisely
matching HTTP requests and generating fake responses.

## Usage

Expand All @@ -35,18 +37,22 @@ configuration method for the HTTP method you want to map a request to.

You can configure fakes responses using a specific path or a URI
template. You can also use the request object to customize your
response.
response. The easiest way to configure a response is with the
`charlatanResponse` helper function.

```dart
final charlatan = Charlatan();
charlatan.whenPost('/users', (_) => { 'id': 1, 'bilbo' });
charlatan.whenGet('/users/{id}', (req) => { 'id': req.pathParameters['id'], 'name': 'bilbo' });
charlatan.whenPut('/users/{id}/profile', (_) => null, statusCode: 204);
charlatan.whenDelete('/users/{id}', (_) => null, statusCode: 204);
charlatan.whenPost('/users', charlatanResponse(body: { 'id': 1, 'bilbo' }));
charlatan.whenGet('/users/{id}', charlatanResponse(body: { 'name': 'bilbo' }));
charlatan.whenPut('/users/{id}/profile', charlatanResponse(statusCode: 204));
charlatan.whenDelete('/users/{id}', (req) => CharlatanHttpResponse(statusCode: 204, body: { 'uri': req.path }));
```

If you need to further customize the response, you can return a
`CharlatanHttpResponse`.
If you need to further customize the response, you can expand
your fake response handler to include whatever you need. The
only requirement is that it returns a `CharlatanHttpResponse`.
This allows you to provide dynamic values for the status code,
body, and headers in the response.

```dart
charlatan.whenPost('/users', (req) {
Expand All @@ -70,6 +76,16 @@ charlatan.whenPost('/users', (req) {
});
```

Additionally, if you need to match requests using other properties of the
request or with different logic, you can use `whenMatch`.

```dart
charlatan.whenMatch(
(req) => req.method == 'GET' && req.path.toLowerCase() == '/posts',
charlatanResponse(statusCode: 200),
);
```

### Building a fake HTTP client

Build the `CharlatanHttpClientAdapter` from the `Charlatan` instance and then
Expand Down
32 changes: 17 additions & 15 deletions example/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ void main() {
});

test('Create a plain fake response', () async {
charlatan.whenGet('/user', (_) => {'name': 'frodo'});
charlatan.whenGet(
'/user',
charlatanResponse(statusCode: 200, body: {'name': 'frodo'}),
);

final plain = await client.get<Object?>('/user');
expect(plain.data, {'name': 'frodo'});
Expand All @@ -28,22 +31,24 @@ void main() {
final template = UriTemplate(pathWithTemplate);
final parser = UriParser(template);
final pathParameters = parser.parse(uri);
return {
'id': pathParameters['id'],
'name': 'frodo',
};
return CharlatanHttpResponse(
statusCode: 200,
body: {
'id': pathParameters['id'],
'name': 'frodo',
},
);
},
);

final withPathParams = await client.get<Object?>('/user/12');
final withPathParams = await client.get<Object?>('/users/12');
expect(withPathParams.data, {'id': '12', 'name': 'frodo'});
});

test('Use a custom status code and an empty body', () async {
charlatan.whenGet(
'/posts',
(_) => null,
statusCode: 204,
charlatanResponse(statusCode: 204),
);

final emptyBody = await client.get<Object?>('/posts');
Expand All @@ -54,8 +59,7 @@ void main() {
test('Use a custom request matcher', () async {
charlatan.whenMatch(
(request) => request.method == 'GET' && request.path == '/posts',
(_) => null,
statusCode: 204,
charlatanResponse(statusCode: 204),
);

final emptyBody = await client.get<Object?>('/posts');
Expand All @@ -69,8 +73,7 @@ void main() {
requestMatchesHttpMethod('GET'),
requestMatchesPathOrTemplate('/posts'),
]),
(_) => null,
statusCode: 204,
charlatanResponse(statusCode: 204),
);

final emptyBody = await client.get<Object?>('/posts');
Expand Down Expand Up @@ -128,13 +131,12 @@ void main() {
(request) {
final params = request.body as Map<String, Object?>? ?? {};
posts.add({'name': params['name']});
return null;
return CharlatanHttpResponse(statusCode: 204);
},
statusCode: 204,
)
..whenGet(
'/posts',
(_) => {'posts': posts},
charlatanResponse(body: {'posts': posts}),
);

final beforeCreatePost = await client.get<Object?>('/posts');
Expand Down
1 change: 1 addition & 0 deletions lib/charlatan.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export 'src/charlatan_response_definition.dart'
CharlatanHttpRequest,
CharlatanHttpResponse,
CharlatanRequestMatcher,
charlatanResponse,
requestMatchesAll,
requestMatchesHttpMethod,
requestMatchesPathOrTemplate;
Expand Down
26 changes: 8 additions & 18 deletions lib/src/charlatan.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class Charlatan {
void whenMatch(
CharlatanRequestMatcher requestMatcher,
CharlatanResponseBuilder responseBuilder, {
int statusCode = 200,
String? description,
}) {
_matchers.insert(
Expand All @@ -38,7 +37,6 @@ class Charlatan {
description: description ?? 'Custom Matcher',
requestMatcher: requestMatcher,
responseBuilder: responseBuilder,
defaultStatusCode: statusCode,
),
);
}
Expand All @@ -48,9 +46,8 @@ class Charlatan {
/// the [statusCode] will be used.
void whenGet(
String pathOrTemplate,
CharlatanResponseBuilder responseBuilder, {
int statusCode = 200,
}) {
CharlatanResponseBuilder responseBuilder,
) {
_matchers.insert(
0,
CharlatanResponseDefinition(
Expand All @@ -60,7 +57,6 @@ class Charlatan {
requestMatchesPathOrTemplate(pathOrTemplate),
]),
responseBuilder: responseBuilder,
defaultStatusCode: statusCode,
),
);
}
Expand All @@ -70,9 +66,8 @@ class Charlatan {
/// the [statusCode] will be used.
void whenPost(
String pathOrTemplate,
CharlatanResponseBuilder responseBuilder, {
int statusCode = 200,
}) {
CharlatanResponseBuilder responseBuilder,
) {
_matchers.insert(
0,
CharlatanResponseDefinition(
Expand All @@ -82,7 +77,6 @@ class Charlatan {
requestMatchesPathOrTemplate(pathOrTemplate),
]),
responseBuilder: responseBuilder,
defaultStatusCode: statusCode,
),
);
}
Expand All @@ -92,9 +86,8 @@ class Charlatan {
/// the [statusCode] will be used.
void whenPut(
String pathOrTemplate,
CharlatanResponseBuilder responseBuilder, {
int statusCode = 200,
}) {
CharlatanResponseBuilder responseBuilder,
) {
_matchers.insert(
0,
CharlatanResponseDefinition(
Expand All @@ -104,7 +97,6 @@ class Charlatan {
requestMatchesPathOrTemplate(pathOrTemplate),
]),
responseBuilder: responseBuilder,
defaultStatusCode: statusCode,
),
);
}
Expand All @@ -114,9 +106,8 @@ class Charlatan {
/// the [statusCode] will be used.
void whenDelete(
String pathOrTemplate,
CharlatanResponseBuilder responseBuilder, {
int statusCode = 200,
}) {
CharlatanResponseBuilder responseBuilder,
) {
_matchers.insert(
0,
CharlatanResponseDefinition(
Expand All @@ -126,7 +117,6 @@ class Charlatan {
requestMatchesPathOrTemplate(pathOrTemplate),
]),
responseBuilder: responseBuilder,
defaultStatusCode: statusCode,
),
);
}
Expand Down
44 changes: 21 additions & 23 deletions lib/src/charlatan_response_definition.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,28 @@ typedef CharlatanRequestMatcher = bool Function(
/// {@template charlatan_response_builder}
/// A type representing a function to convert an http request into a response.
///
/// The return value may be any json encodable object, a [CharlatanHttpResponse],
/// or null.
///
/// Typically you will want to return a json response of Map<String, Object?>
/// which will be automatically serialized and returned with a 200 status code.
///
/// If you want to customize the headers or the status code of the response, you
/// can return an instance of [CharlatanHttpResponse].
/// The return value is a [CharlatanHttpResponse].
/// {@endtemplate}
typedef CharlatanResponseBuilder = FutureOr<Object?> Function(
typedef CharlatanResponseBuilder = FutureOr<CharlatanHttpResponse> Function(
/// The request including path params, body, headers, options, etc
CharlatanHttpRequest request,
);

/// {@template charlatan_response}
/// A function to build a [CharlatanResponseBuilder]. The [statusCode] defaults
/// to 200, the [body] defaults to null, and the [headers] defaults to empty.
/// {@endtemplate}
CharlatanResponseBuilder charlatanResponse({
int statusCode = 200,
Object? body,
Map<String, String> headers = const {},
}) =>
(request) => CharlatanHttpResponse(
statusCode: statusCode,
body: body,
headers: headers,
);

/// {@template charlatan_matches_all}
/// A function to build a [CharlatanRequestMatcher] that matches if all of the
/// provided [CharlatanRequestMatcher]s match.
Expand Down Expand Up @@ -84,18 +92,13 @@ class CharlatanResponseDefinition {
/// The callback that produces the response.
final CharlatanResponseBuilder responseBuilder;

/// The default status code to use if the [responseBuilder] does not return
/// a [CharlatanHttpResponse].
final int defaultStatusCode;

/// A description of the response definition, e.g. GET /users/123
final String description;

/// {@macro charlatan_http_response_definition}
CharlatanResponseDefinition({
required this.requestMatcher,
required this.responseBuilder,
required this.defaultStatusCode,
required this.description,
});

Expand All @@ -107,16 +110,11 @@ class CharlatanResponseDefinition {
Future<CharlatanHttpResponse> buildResponse(
CharlatanHttpRequest request,
) async {
final result = await responseBuilder(request);

if (result is CharlatanHttpResponse) {
return result;
final responseOrFuture = responseBuilder(request);
if (responseOrFuture is Future<CharlatanHttpResponse>) {
return await responseOrFuture;
}

return CharlatanHttpResponse(
body: result,
statusCode: defaultStatusCode,
);
return responseOrFuture;
}
}

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: charlatan
description: A library for configuring and providing fake HTTP responses to your dio HTTP client.
version: 0.3.1
version: 0.4.0
homepage: https://github.com/Betterment/charlatan
repository: https://github.com/Betterment/charlatan

Expand Down
Loading

0 comments on commit 671566f

Please sign in to comment.