diff --git a/TESTING.md b/TESTING.md index 56be291133..7a90f7d3eb 100644 --- a/TESTING.md +++ b/TESTING.md @@ -86,6 +86,91 @@ are cases where multiple calls represent a single behavior (e.g. "when the WebSo disconnects and then reconnects, missed messages are re-fetched") — use your judgment. A larger number of shorter tests beats a smaller number of longer ones. +## Assert thrown errors with `throwsA`, never a bare `try`/`catch` + +A `try`/`catch` that asserts inside the `catch` asserts *nothing* when the code stops +throwing: the `catch` body never runs, and the test stays green. + +```dart +// BAD — still passes after the `name` setter's guard is deleted. +test('should throw if trying to set `name`', () { + try { + channel.name = 'New name'; + } catch (e) { + expect(e, isA()); + } +}); +``` + +Use `throwsA`, which fails when nothing is thrown: + +```dart +// GOOD — synchronous throw. +expect(() => channel.name = 'New name', throwsA(isA())); + +// GOOD — asynchronous throw. +await expectLater( + channel.markUnread('message-id-123'), + throwsA(isA()), +); +``` + +An error can leave a call two ways, and they need different forms: + +1. **Thrown synchronously** — the call raises before it returns a `Future` at all. +2. **Delivered asynchronously** — the call returns a `Future` that completes with an + error. + +`throwsA` covers both, but only when it can invoke the call itself. Passing a future +directly only ever covers the second case: in the first, the call throws while Dart is +still evaluating the argument, so `expectLater` never runs and the error escapes as a +raw failure instead of a matcher message. + +You cannot tell the two apart from a signature — `Future foo()` can throw +synchronously, `Future foo() async` never does. Anything before the first `await`, +including an `assert`, runs eagerly. So wrap the call in a closure whenever it might +throw synchronously, and whenever you are not sure: + +```dart +// `pinMessage` is not `async`, and validates in an `assert` before its first +// `await` — so it throws synchronously even though it returns a `Future`. +await expectLater( + () => channel.pinMessage(message, timeoutOrExpirationDate: 'invalid'), + throwsA(isA()), +); +``` + +For an `async` method, which can only fail the second way, passing the future directly +(as in the `markUnread` example above) is fine and reads better. + +To assert on the error's fields, compose the matcher with `having` instead of casting +inside a `catch`: + +```dart +await expectLater( + channel.sendMessage(message), + throwsA( + isA() + .having((it) => it.code, 'code', ChatErrorCode.notAllowed.code), + ), +); +``` + +Where `throwsA` does not fit — an error raised inside a widget `builder`, or one wrapped +in a type you have to unwrap before asserting — capture the error and assert **outside** +the `try`, so a missing throw leaves the variable `null` and the matcher fails: + +```dart +Object? caught; +try { + StreamChannel.of(context); +} catch (error) { + caught = error; +} + +expect(caught, isA()); +``` + ## Only include relevant details in a test Tests often need setup that isn't part of the behavior under test. When that setup diff --git a/packages/stream_chat/test/src/client/channel/channel_test.dart b/packages/stream_chat/test/src/client/channel/channel_test.dart index d7f3b27a86..21aa70487c 100644 --- a/packages/stream_chat/test/src/client/channel/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel/channel_test.dart @@ -257,27 +257,15 @@ void main() { }); test('should throw if trying to set `extraData`', () { - try { - channel.extraData = {'name': 'test-channel-name'}; - } catch (e) { - expect(e, isA()); - } + expect(() => channel.extraData = {'name': 'test-channel-name'}, throwsA(isA())); }); test('should throw if trying to set `image`', () { - try { - channel.image = 'https://stream.io/some-image'; - } catch (e) { - expect(e, isA()); - } + expect(() => channel.image = 'https://stream.io/some-image', throwsA(isA())); }); test('should throw if trying to set `name`', () { - try { - channel.name = 'New name'; - } catch (e) { - expect(e, isA()); - } + expect(() => channel.name = 'New name', throwsA(isA())); }); group('`.sendMessage`', () { @@ -373,17 +361,15 @@ void main() { ]), ); - try { - await channel.sendMessage( + await expectLater( + channel.sendMessage( message, skipPush: true, - ); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); - } + ), + throwsA( + isA().having((it) => it.code, 'code', ChatErrorCode.notAllowed.code), + ), + ); }, ); @@ -430,18 +416,16 @@ void main() { ]), ); - try { - await channel.sendMessage( + await expectLater( + channel.sendMessage( message, skipPush: true, skipEnrichUrl: true, - ); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); - } + ), + throwsA( + isA().having((it) => it.code, 'code', ChatErrorCode.notAllowed.code), + ), + ); }, ); @@ -487,17 +471,15 @@ void main() { ]), ); - try { - await channel.sendMessage( + await expectLater( + channel.sendMessage( message, skipEnrichUrl: true, - ); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); - } + ), + throwsA( + isA().having((it) => it.code, 'code', ChatErrorCode.notAllowed.code), + ), + ); }, ); @@ -542,16 +524,14 @@ void main() { ]), ); - try { - await channel.sendMessage( + await expectLater( + channel.sendMessage( message, - ); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); - } + ), + throwsA( + isA().having((it) => it.code, 'code', ChatErrorCode.notAllowed.code), + ), + ); }, ); @@ -603,11 +583,10 @@ void main() { ]), ); - try { - await channel.sendMessage(message); - } catch (e) { - expect(e, isA()); - } + await expectLater( + channel.sendMessage(message), + throwsA(isA()), + ); }); test('with attachments should work just fine', () async { @@ -1671,11 +1650,10 @@ void main() { ]), ); - try { - await channel.updateMessage(message, skipEnrichUrl: true); - } catch (e) { - expect(e, isA()); - } + await expectLater( + channel.updateMessage(message, skipEnrichUrl: true), + throwsA(isA()), + ); }); test( @@ -1723,15 +1701,14 @@ void main() { ]), ); - try { - await channel.updateMessage(message, skipEnrichUrl: true); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.requestTimeout.code)); - expect(networkError.isRetriable, isTrue); - } + await expectLater( + channel.updateMessage(message, skipEnrichUrl: true), + throwsA( + isA() + .having((it) => it.code, 'code', ChatErrorCode.requestTimeout.code) + .having((it) => it.isRetriable, 'isRetriable', isTrue), + ), + ); }, ); @@ -1780,15 +1757,14 @@ void main() { ]), ); - try { - await channel.updateMessage(message, skipPush: true); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.internalSystemError.code)); - expect(networkError.isRetriable, isTrue); - } + await expectLater( + channel.updateMessage(message, skipPush: true), + throwsA( + isA() + .having((it) => it.code, 'code', ChatErrorCode.internalSystemError.code) + .having((it) => it.isRetriable, 'isRetriable', isTrue), + ), + ); }, ); @@ -1830,18 +1806,16 @@ void main() { ]), ); - try { - await channel.updateMessage( + await expectLater( + channel.updateMessage( message, skipPush: true, skipEnrichUrl: true, - ); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); - } + ), + throwsA( + isA().having((it) => it.code, 'code', ChatErrorCode.notAllowed.code), + ), + ); }); test('should handle non-retriable StreamChatNetworkError with skipPush: false, skipEnrichUrl: false', () async { @@ -1880,14 +1854,12 @@ void main() { ]), ); - try { - await channel.updateMessage(message); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); - } + await expectLater( + channel.updateMessage(message), + throwsA( + isA().having((it) => it.code, 'code', ChatErrorCode.notAllowed.code), + ), + ); }); }); @@ -2053,15 +2025,14 @@ void main() { ]), ); - try { - await channel.partialUpdateMessage( + await expectLater( + channel.partialUpdateMessage( message, set: set, unset: unset, - ); - } catch (e) { - expect(e, isA()); - } + ), + throwsA(isA()), + ); }); test( @@ -2122,20 +2093,19 @@ void main() { ]), ); - try { - await channel.partialUpdateMessage( + await expectLater( + channel.partialUpdateMessage( message, set: set, unset: unset, skipEnrichUrl: true, - ); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.requestTimeout.code)); - expect(networkError.isRetriable, isTrue); - } + ), + throwsA( + isA() + .having((it) => it.code, 'code', ChatErrorCode.requestTimeout.code) + .having((it) => it.isRetriable, 'isRetriable', isTrue), + ), + ); }, ); @@ -2196,19 +2166,18 @@ void main() { ]), ); - try { - await channel.partialUpdateMessage( + await expectLater( + channel.partialUpdateMessage( message, set: set, unset: unset, - ); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.internalSystemError.code)); - expect(networkError.isRetriable, isTrue); - } + ), + throwsA( + isA() + .having((it) => it.code, 'code', ChatErrorCode.internalSystemError.code) + .having((it) => it.isRetriable, 'isRetriable', isTrue), + ), + ); }, ); @@ -2262,19 +2231,17 @@ void main() { ]), ); - try { - await channel.partialUpdateMessage( + await expectLater( + channel.partialUpdateMessage( message, set: set, unset: unset, skipEnrichUrl: true, - ); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); - } + ), + throwsA( + isA().having((it) => it.code, 'code', ChatErrorCode.notAllowed.code), + ), + ); }); test('should handle non-retriable StreamChatNetworkError with skipEnrichUrl: false', () async { @@ -2326,18 +2293,16 @@ void main() { ]), ); - try { - await channel.partialUpdateMessage( + await expectLater( + channel.partialUpdateMessage( message, set: set, unset: unset, - ); - } catch (e) { - expect(e, isA()); - - final networkError = e as StreamChatNetworkError; - expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); - } + ), + throwsA( + isA().having((it) => it.code, 'code', ChatErrorCode.notAllowed.code), + ), + ); }); }); @@ -2713,14 +2678,15 @@ void main() { final message = Message(id: 'test-message-id'); const timeoutOrExpirationDate = 'invalid-value'; - try { - await channel.pinMessage( + // `pinMessage` validates in an `assert` before its first `await`, so + // it throws synchronously and the call must stay in a closure. + await expectLater( + () => channel.pinMessage( message, timeoutOrExpirationDate: timeoutOrExpirationDate, - ); - } catch (e) { - expect(e, isA()); - } + ), + throwsA(isA()), + ); }, ); }); @@ -3014,11 +2980,10 @@ void main() { ]), ); - try { - await channel.sendReaction(message, reaction); - } catch (e) { - expect(e, isA()); - } + await expectLater( + channel.sendReaction(message, reaction), + throwsA(isA()), + ); verify(() => client.sendReaction(message.id, reaction)).called(1); }, @@ -3220,11 +3185,10 @@ void main() { ]), ); - try { - await channel.sendReaction(message, reaction); - } catch (e) { - expect(e, isA()); - } + await expectLater( + channel.sendReaction(message, reaction), + throwsA(isA()), + ); verify(() => client.sendReaction(message.id, reaction)).called(1); }, @@ -3419,11 +3383,10 @@ void main() { ]), ); - try { - await channel.deleteReaction(message, reaction); - } catch (e) { - expect(e, isA()); - } + await expectLater( + channel.deleteReaction(message, reaction), + throwsA(isA()), + ); verify(() => client.deleteReaction(messageId, type)).called(1); }, @@ -3547,11 +3510,10 @@ void main() { ]), ); - try { - await channel.deleteReaction(message, reaction); - } catch (e) { - expect(e, isA()); - } + await expectLater( + channel.deleteReaction(message, reaction), + throwsA(isA()), + ); verify(() => client.deleteReaction(messageId, type)).called(1); }, @@ -4044,11 +4006,10 @@ void main() { ), ).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); - try { - await channel.watch(); - } catch (e) { - expect(e, isA()); - } + await expectLater( + channel.watch(), + throwsA(isA()), + ); verify( () => client.queryChannel( @@ -4242,11 +4203,10 @@ void main() { ), ).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); - try { - await channel.query(); - } catch (e) { - expect(e, isA()); - } + await expectLater( + channel.query(), + throwsA(isA()), + ); verify( () => client.queryChannel( diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 8f2f4483f8..0000614a24 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -117,11 +117,10 @@ void main() { ]), ); - try { - await client.connectGuestUser(user); - } catch (e) { - expect(e, isA()); - } + await expectLater( + client.connectGuestUser(user), + throwsA(isA()), + ); verify( () => api.guest.getGuestUser(any(that: isSameUserAs(user))), @@ -146,29 +145,28 @@ void main() { group('`.openConnection`', () { test('should throw if state does not contain user', () async { expect(client.state.currentUser, isNull); - try { - await client.openConnection(); - } catch (e) { - expect(e, isA()); - } + await expectLater( + client.openConnection(), + throwsA(isA()), + ); }); test('should throw if connection is already available', () async { expect(client.state.currentUser, isNull); - try { - await client.connectAnonymousUser(); - // waiting 300ms for `wsConnectionStatusStream` to emit - await delay(300); + await client.connectAnonymousUser(); + // waiting 300ms for `wsConnectionStatusStream` to emit + await delay(300); - await client.openConnection(); - } catch (e) { - expect(e, isA()); - final err = e as StreamChatError; - expect( - err.message.contains('Connection already available for'), - isTrue, - ); - } + await expectLater( + client.openConnection(), + throwsA( + isA().having( + (it) => it.message, + 'message', + contains('Connection already available for'), + ), + ), + ); }); test('should open connection for closed connection', () async { @@ -221,11 +219,10 @@ void main() { final user = User(id: 'test-user-id'); final token = Token.development(user.id).rawValue; - try { - await client.connectUser(user, token); - } catch (e) { - expect(e, isA()); - } + await expectLater( + client.connectUser(user, token), + throwsA(isA()), + ); }); test( @@ -237,11 +234,10 @@ void main() { return Token.development(userId).rawValue; } - try { - await client.connectUserWithProvider(user, tokenProvider); - } catch (e) { - expect(e, isA()); - } + await expectLater( + client.connectUserWithProvider(user, tokenProvider), + throwsA(isA()), + ); }, ); @@ -255,11 +251,10 @@ void main() { ..accessToken = token, ); - try { - await client.connectGuestUser(user); - } catch (e) { - expect(e, isA()); - } + await expectLater( + client.connectGuestUser(user), + throwsA(isA()), + ); verify( () => api.guest.getGuestUser(any(that: isSameUserAs(user))), ).called(1); @@ -268,11 +263,10 @@ void main() { test( '`.connectAnonymousUser` should throw if `ws.connect` fails', () async { - try { - await client.connectAnonymousUser(); - } catch (e) { - expect(e, isA()); - } + await expectLater( + client.connectAnonymousUser(), + throwsA(isA()), + ); }, ); }); @@ -4993,14 +4987,15 @@ void main() { const messageId = 'test-message-id'; const timeoutOrExpirationDate = 'invalid-value'; - try { - await client.pinMessage( + // `pinMessage` validates in an `assert` before its first `await`, + // so it throws synchronously and the call must stay in a closure. + await expectLater( + () => client.pinMessage( messageId, timeoutOrExpirationDate: timeoutOrExpirationDate, - ); - } catch (e) { - expect(e, isA()); - } + ), + throwsA(isA()), + ); }, ); }); diff --git a/packages/stream_chat/test/src/core/api/general_api_test.dart b/packages/stream_chat/test/src/core/api/general_api_test.dart index 2c855c83e7..85caafd7b1 100644 --- a/packages/stream_chat/test/src/core/api/general_api_test.dart +++ b/packages/stream_chat/test/src/core/api/general_api_test.dart @@ -63,11 +63,10 @@ void main() { 'should throw if `query` and `messageFilters` is not provided', () async { final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); - try { - await generalApi.searchMessages(filter); - } catch (e) { - expect(e, isA()); - } + await expectLater( + generalApi.searchMessages(filter), + throwsA(isA()), + ); }, ); @@ -77,15 +76,14 @@ void main() { final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); const query = 'test-query'; final messageFilter = Filter.query('key', 'text'); - try { - await generalApi.searchMessages( + await expectLater( + generalApi.searchMessages( filter, query: query, messageFilters: messageFilter, - ); - } catch (e) { - expect(e, isA()); - } + ), + throwsA(isA()), + ); }, ); @@ -95,15 +93,14 @@ void main() { final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); const sort = [SortOption.desc('test-field')]; const pagination = PaginationParams(offset: 10); - try { - await generalApi.searchMessages( + await expectLater( + generalApi.searchMessages( filter, sort: sort, pagination: pagination, - ); - } catch (e) { - expect(e, isA()); - } + ), + throwsA(isA()), + ); }, ); diff --git a/packages/stream_chat/test/src/core/api/requests_test.dart b/packages/stream_chat/test/src/core/api/requests_test.dart index 36c25cae8f..0f843042d8 100644 --- a/packages/stream_chat/test/src/core/api/requests_test.dart +++ b/packages/stream_chat/test/src/core/api/requests_test.dart @@ -13,11 +13,10 @@ void main() { test( 'should throw if non-zero `offset` and `next` both are provided', () { - try { - PaginationParams(offset: 10, next: 'next-message-id'); - } catch (e) { - expect(e, isA()); - } + expect( + () => PaginationParams(offset: 10, next: 'next-message-id'), + throwsA(isA()), + ); }, ); diff --git a/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart b/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart index cc92b01a8f..fda3018af0 100644 --- a/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart +++ b/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart @@ -63,16 +63,22 @@ void main() { authInterceptor.onRequest(options, handler); + // The handler rejects with a private dio type, so capture the error and + // assert outside the `try`: a missing rejection leaves `caught` null. + Object? caught; try { await handler.future; } catch (e) { - // need to cast it as the type is private in dio - var error = (e as dynamic).data; - expect(error, isA()); - error = (error as StreamChatDioError).error; - expect(error.code, ChatErrorCode.undefinedToken.code); - expect(error.message, ChatErrorCode.undefinedToken.message); + caught = e; } + + // need to cast it as the type is private in dio + final dioError = (caught as dynamic)?.data; + expect(dioError, isA()); + + final error = (dioError as StreamChatDioError).error; + expect(error.code, ChatErrorCode.undefinedToken.code); + expect(error.message, ChatErrorCode.undefinedToken.message); }, ); @@ -147,14 +153,16 @@ void main() { authInterceptor.onError(err, handler); + Object? caught; try { await handler.future; } catch (e) { - // need to cast it as the type is private in dio - final error = (e as dynamic).data; - expect(error, isA()); + caught = e; } + // need to cast it as the type is private in dio + expect((caught as dynamic)?.data, isA()); + verify(() => tokenManager.isStatic).called(1); verify(() => tokenManager.loadToken(refresh: true)).called(1); @@ -185,16 +193,21 @@ void main() { authInterceptor.onError(err, handler); + Object? caught; try { await handler.future; } catch (e) { - // need to cast it as the type is private in dio - final error = (e as dynamic).data; - expect(error, isA()); - final response = StreamChatNetworkError.fromDioException(error); - expect(response.errorCode, code); + caught = e; } + // need to cast it as the type is private in dio + final error = (caught as dynamic)?.data; + expect(error, isA()); + expect( + StreamChatNetworkError.fromDioException(error as DioException).errorCode, + code, + ); + verify(() => tokenManager.isStatic).called(1); verifyNoMoreInteractions(tokenManager); }, @@ -211,13 +224,15 @@ void main() { authInterceptor.onError(err, handler); + Object? caught; try { await handler.future; } catch (e) { - // need to cast it as the type is private in dio - final error = (e as dynamic).data; - expect(error, isA()); + caught = e; } + + // need to cast it as the type is private in dio + expect((caught as dynamic)?.data, isA()); }, ); } diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart index 936af1670b..637badf444 100644 --- a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart +++ b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart @@ -128,9 +128,10 @@ void main() { final logger = MockLogger(); final client = StreamHttpClient(apiKey, logger: logger); - try { - await client.get('path'); - } catch (_) {} + await expectLater( + client.get('path'), + throwsA(isA()), + ); verify(() => logger.info(any())).called(greaterThan(0)); }); @@ -140,9 +141,10 @@ void main() { final logger = MockLogger(); final client = StreamHttpClient(apiKey, logger: logger); - try { - await client.get('path'); - } catch (_) {} + await expectLater( + client.get('path'), + throwsA(isA()), + ); verify(() => logger.severe(any())).called(greaterThan(0)); }); @@ -150,17 +152,18 @@ void main() { test('`.close` should close the dio client', () async { final client = StreamHttpClient('api-key')..close(force: true); - try { - await client.get('path'); - } on StreamChatNetworkError catch (e) { - expect(e, isA()); - expect( - e.message, - "The connection errored: Dio can't establish a new connection" - ' after it was closed. This indicates an error which most likely' - ' cannot be solved by the library.', - ); - } + await expectLater( + client.get('path'), + throwsA( + isA().having( + (it) => it.message, + 'message', + "The connection errored: Dio can't establish a new connection" + ' after it was closed. This indicates an error which most likely' + ' cannot be solved by the library.', + ), + ), + ); }); test('`.get` should return response successfully', () async { @@ -206,12 +209,15 @@ void main() { ), ).thenThrow(error); - try { - await client.get(path); - } catch (e) { - expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioException(error)); - } + await expectLater( + client.get(path), + throwsA( + allOf( + isA(), + equals(StreamChatNetworkError.fromDioException(error)), + ), + ), + ); verify( () => dio.get( @@ -267,12 +273,15 @@ void main() { ), ).thenThrow(error); - try { - await client.post(path); - } catch (e) { - expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioException(error)); - } + await expectLater( + client.post(path), + throwsA( + allOf( + isA(), + equals(StreamChatNetworkError.fromDioException(error)), + ), + ), + ); verify( () => dio.post( @@ -329,12 +338,15 @@ void main() { ), ).thenThrow(error); - try { - await client.delete(path); - } catch (e) { - expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioException(error)); - } + await expectLater( + client.delete(path), + throwsA( + allOf( + isA(), + equals(StreamChatNetworkError.fromDioException(error)), + ), + ), + ); verify( () => dio.delete( @@ -391,12 +403,15 @@ void main() { ), ).thenThrow(error); - try { - await client.patch(path); - } catch (e) { - expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioException(error)); - } + await expectLater( + client.patch(path), + throwsA( + allOf( + isA(), + equals(StreamChatNetworkError.fromDioException(error)), + ), + ), + ); verify( () => dio.patch( @@ -453,12 +468,15 @@ void main() { ), ).thenThrow(error); - try { - await client.put(path); - } catch (e) { - expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioException(error)); - } + await expectLater( + client.put(path), + throwsA( + allOf( + isA(), + equals(StreamChatNetworkError.fromDioException(error)), + ), + ), + ); verify( () => dio.put( @@ -522,12 +540,15 @@ void main() { ), ).thenThrow(error); - try { - await client.postFile(path, file); - } catch (e) { - expect(e, isA()); - expect(e, StreamChatNetworkError.fromDioException(error)); - } + await expectLater( + client.postFile(path, file), + throwsA( + allOf( + isA(), + equals(StreamChatNetworkError.fromDioException(error)), + ), + ), + ); verify( () => dio.post( @@ -586,12 +607,12 @@ void main() { ), ).thenThrow(error); - try { - await client.request(path); - } catch (e) { - expect(e, isA()); - expect(e, error.error); - } + await expectLater( + client.request(path), + throwsA( + allOf(isA(), equals(error.error)), + ), + ); verify( () => dio.request( diff --git a/packages/stream_chat/test/src/core/http/token_manager_test.dart b/packages/stream_chat/test/src/core/http/token_manager_test.dart index 626fd676b4..5affb7df1a 100644 --- a/packages/stream_chat/test/src/core/http/token_manager_test.dart +++ b/packages/stream_chat/test/src/core/http/token_manager_test.dart @@ -49,11 +49,10 @@ void main() { expect(tokenManager.userId, isNull); const userId = 'test-user-id'; - try { - await tokenManager.setTokenOrProvider(userId); - } catch (e) { - expect(e, isA()); - } + await expectLater( + tokenManager.setTokenOrProvider(userId), + throwsA(isA()), + ); }, ); @@ -65,15 +64,14 @@ void main() { const userId = 'test-user-id'; final token = Token.development(userId); Future tokenProvider(String userId) async => Token.development(userId).rawValue; - try { - await tokenManager.setTokenOrProvider( + await expectLater( + tokenManager.setTokenOrProvider( userId, token: token, provider: tokenProvider, - ); - } catch (e) { - expect(e, isA()); - } + ), + throwsA(isA()), + ); }, ); diff --git a/packages/stream_chat/test/src/core/http/token_test.dart b/packages/stream_chat/test/src/core/http/token_test.dart index 543ffda6ba..5bceedc26f 100644 --- a/packages/stream_chat/test/src/core/http/token_test.dart +++ b/packages/stream_chat/test/src/core/http/token_test.dart @@ -20,13 +20,21 @@ void main() { expect(token, devToken); }); + test('`.fromRawValue` should throw if the raw value is not a valid JWT', () { + const notAJwt = 'bad-token-without-a-user-id'; + expect(() => Token.fromRawValue(notAJwt), throwsA(isA())); + }); + test('`.fromRawValue` should throw if does not contain `user_id`', () { - const badToken = 'bad-token-without-a-user-id'; - try { - Token.fromRawValue(badToken); - } catch (e) { - expect(e, isA()); - } + // A well-formed JWT whose payload carries no `user_id` claim. + const header = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'; + const payload = 'eyJmb28iOiJiYXIifQ'; + const tokenWithoutUserId = '$header.$payload.devtoken'; + + expect( + () => Token.fromRawValue(tokenWithoutUserId), + throwsA(isA()), + ); }); test('`.development` should create a dev-token with provided user-id', () { diff --git a/packages/stream_chat/test/src/ws/websocket_test.dart b/packages/stream_chat/test/src/ws/websocket_test.dart index 29f4d246f3..f4c039442a 100644 --- a/packages/stream_chat/test/src/ws/websocket_test.dart +++ b/packages/stream_chat/test/src/ws/websocket_test.dart @@ -200,13 +200,14 @@ void main() { test('`connect` should throw if already in connection attempt', () async { final user = OwnUser(id: 'test-user'); - webSocket.connect(user); - try { - // calling again before previous attempt finishes - await webSocket.connect(user); - } catch (e) { - expect(e, isA()); - } + unawaited(webSocket.connect(user)); + + // calling again before previous attempt finishes. The guard throws + // synchronously, so the call has to stay inside a closure. + expect( + () => webSocket.connect(user), + throwsA(isA()), + ); }); test('`connect` should throw if `onMessage` contains error', () async { @@ -228,14 +229,14 @@ void main() { ]), ); - try { - await webSocket.connect(user); - } catch (e) { - expect(e, isA()); - final err = e as StreamWebSocketError; - expect(err.code, error.code); - expect(err.message, error.message); - } + await expectLater( + webSocket.connect(user), + throwsA( + isA() + .having((it) => it.code, 'code', error.code) + .having((it) => it.message, 'message', error.message), + ), + ); addTearDown(timer.cancel); });