How to test Function passed as an argument in Flutter?
code:
Future<User> execute({
required String username,
required String password,
required void Function(AuthFailure fail) onFailure,
required void Function(User user) onSuccess,
}) async {
if (username.isNonValid || password.isNonValid) {
onFailure(const AuthFailure.wrongCredentials()); // I want to test this line
return const User.anonymous();
}
...
}
test:
test('use case - failure execution for incorrect credentials', () async {
// GIVEN
// WHEN
final user = await useCase.execute(
username: "noname",
password: "password",
onFailure: (fail) {},
onSuccess: (user) {},
);
// THEN
// TODO how to verify onFailure call inside useCase?
expect(user, const User.anonymous());
});
Or maybe testing this way is not the idiomatic way, because the test becomes more white-box instead black-box? Should I perceive passing functions as arguments to use cases as anti-pattern? I can change it then. The proposition is to return sth like Either from useCase.execute():
Future<Either<Failure, Success>> execute({
required String username,
required String password,
}) async {
if (username.isEmpty || password.isEmpty) {
// return wrapper around AuthFailure.wrongCredentials()) of Either left subtype (Either has two subtypes)
}
...
}
This way I only verify return type, and all the lines are covered this way. It's gonna work, but I feel better with the simplest, not the smartest solution.
PS I use Mocktail for mocking, but using Mockito in solution is also warmly welcomed.
If you just want to verify that the callback is triggered, I personally would just make your callback set a flag and then test that flag afterward, which I think is straightforward, simple, and easy to understand with no magic:
test('use case - failure execution for incorrect credentials', () async {
var failureCalled = false;
final user = await useCase.execute(
username: "noname",
password: "password",
onFailure: (fail) => failureCalled = true,
onSuccess: (user) {},
);
expect(user, const User.anonymous());
expect(failureCalled, true);
});
But if you really want to use Mocks, you will need some Mock object to use and to call instance methods on that in callbacks. With Mockito you could do:
test('use case - failure execution for incorrect credentials', () async {
dynamic mock = Mock();
final user = await useCase.execute(
username: "noname",
password: "password",
onFailure: (fail) => mock.fail(fail),
onSuccess: (user) {},
);
expect(user, const User.anonymous());
verify(mock.fail(any)).called(1);
});
Some things to note:
To avoid declaring a class with the expected instance methods and then code-generating stubs, create a raw Mock instance but declare it as dynamic to disable static type-checking. This will then take advantage the Mock.noSuchMethod implementation.
You can't use onFailure: mock.fail directly since the Mock has no generated stubs, and mock.fail will just be null instead of a Function.
I am not experienced with Mocktail, but I imagine that you could do something similar.
Related
I am trying to write unit tests for a flutter app and I can't get this one test case to work correctly.
Here is the function returning Future<Either<WeatherData, DataError>>:
#override
Future<Either<WeatherData, DataError>> fetchWeatherByCity({required String city}) async {
try {
var response = await apiService.fetchWeatherByCity(city: city);
if (response.statusCode == 200) {
return Left(WeatherData.fromJson(jsonDecode(response.body)));
} else {
return Right(DataError(title: "Error", description: "Desc", code: 0, url: "NoUrl"));
}
} catch (error) {
AppException exception = error as AppException;
return Right(DataError(
title: exception.title, description: exception.description, code: exception.code, url: exception.url));
}
}
Here is the code where I am trying to write the unit test:
sut = WeatherRepositoryImpl(apiService: mockWeatherApiService);
test(
"get weather by city DataError 1 - Error 404 ",
() async {
when(mockWeatherApiService.fetchWeatherByCity(city: "city"))
.thenAnswer((_) async => Future.value(weatherRepoMockData.badResponse));
final result = await sut.fetchWeatherByCity(city: "city");
verify(mockWeatherApiService.fetchWeatherByCity(city: "city")).called(1);
expect(result, isInstanceOf<DataError>);
verifyNoMoreInteractions(mockWeatherApiService);
},
);
When I run this specific test, I receive this error:
Expected: <Instance of 'DataError'>
Actual: Right<WeatherData, DataError>:<Right(Instance of 'DataError')>
Which: is not an instance of 'DataError'
What I am not getting here? What should I be expecting from the function for the test to pass successfully?
You are directly using the result which is actually a wrapper and has a type of Either<WeatherData, DataError>.
You need to unwrap the value using the fold method on the result and then expect accordingly, So in your code you can do something like this to make it work:
final result = await sut.fetchWeatherByCity(city: "city");
result.fold(
(left) => fail('test failed'),
(right) {
expect(result, isInstanceOf<DataError>);
});
verifyNoMoreInteractions(mockWeatherApiService);
Hope this helps.
You need to either make the expected value a Right(), or extract the right side of the actual value. Doing either of those will match, but as it is, you're comparing a wrapped value with an unwrapped value.
I am trying to mock aws-sdk with jest. Actually I only care about one function. How can I do this? I have read the docs about mocking classes with jest, but the docs are complicated and I don't quite understand them.
Here is my best attempt:
handler.test.js
'use strict';
const aws = require('aws-sdk');
const { handler } = require('../../src/rotateSecret/index');
jest.mock('aws-sdk');
const event = {
SecretId: 'test',
ClientRequestToken: 'ccc',
Step: 'createSecret',
};
describe('rotateSecret', () => {
it.only('should not get or put a secret', async () => {
aws.SecretsManager.mockImplementation(() => ({
getSecretValue: () => ({}),
}));
expect.assertions(1);
await handler(event);
// You can see what I am trying to do here but it doesn't work
expect(aws.SecretsManager.getSecretManager).not.toHaveBeenCalled();
});
});
handler.js
exports.handler = async (event) => {
const secretsManager = new aws.SecretsManager();
const secret = await secretsManager.describeSecret({ SecretId: event.SecretId }).promise();
if (someCondition) {
console.log("All conditions not met");
return;
}
return secretsManager.getSecretValue(someParams)
};
Okay so the way I would approach this is as follows:
AWS-SDK Mock
Create an actual mock for aws-sdk and put it in __mocks__/aws-sdk.js file at the root of your project
// __mocks__/aws-sdk.js
class AWS {
static SecretsManager = class {
describeSecret = jest.fn(() =>{
return { promise: ()=> Promise.resolve({ ARN: "custom-arn1", Name: "describeSec" })}
});
getSecretValue = jest.fn(() =>{
return {promise: ()=> Promise.resolve({ ARN: "custom-arn2", Name: "getSecretVal" })
});
};
}
module.exports = AWS;
I have used static before SecretsManager because AWS class is never instantiated yet it wants access to SecretsManager class.
Inside SecretsManager, I have defined 2 functions and stubbed them using jest.fn.
Now same stuff as you have done in your test file:
jest.mock('aws-sdk');
How to Test
To test if your mock functions are called, thats the tricky part (so i will detail that at the end of this post).
Better approach would be to assert against the end result of your main function after all processing is finished.
Assertions
Back in your test file, I would simply invoke the handler with the await (as you already have) and then assert against the final result like so:
// test.js
describe("rotateSecret", () => {
it.only("should not get or put a secret", async () => {
const event = {name:"event"};
const result = await handler(event);
expect(result).toEqual("whatever-your-function-is-expected-to-return");
});
});
Testing Secret Manager's function invocations
For this you will need to tweak your main handler.js file itself and will need to take out invocation of secrets Manager from the main function body like so:
const secretsManager = new aws.SecretsManager(); // <---- Declare it in outer scope
exports.handler = async (event) => {
const secret = await secretsManager
.describeSecret({ SecretId: event.SecretId })
.promise();
if (someCondition) {
console.log("All conditions not met");
return;
}
return secretsManager.getSecretValue(someParams);
};
Then back in your test.js file, you will need to similarly declare the SecretsManager invocation before you initiate your handler function like so:
//test.js
describe("rotateSecret", () => {
const secretsManager = new aws.SecretsManager(); // <---- Declare it in outer scope
it.only("should not get or put a secret", async () => {
const event = {name:"event"};
await handler(event);
// Now you can make assertions on function invocations
expect(secretsManager.describeSecret).toHaveBeenCalled();
// OR check if passed args were correct
expect(secretsManager.describeSecret).toHaveBeenCalledWith({
SecretId: event.SecretId,
});
});
});
This will allow you to make assertions on function invocation as well the args that were passed.
The reason I declare it outside function scope is to tell Jest that secretsManager should be existing somewhere in global scope and it should be used from there.
Previously, we had it declared inside the function scope, so Jest would invoke it but we weren't able to get access to it.
We couldn't directly reference it like this AWS.SecretsManager.getSecretManager because getSecretManager method is only available after you instantiate the SecretsManager class (and even if you did that, you will get a new instance of the class which won't help with any assertions).
Downside of __mocks__/aws.js fake module
Obvious issue is - you are stubbing the function on every single call and maybe you won't want that.
Perhaps you only want to stub it out once for a specific test but for the rest of them you want it to run normal.
In that case, you should not create __mocks__ folder.
Instead, create a one-time fake BUT make sure your SecretsManager invocation is in the outside scope in your test file as before.
//test.js
const aws = require("aws-sdk");
describe("rotateSecret", () => {
// Declare it in outer scope
const secretsManager = new aws.SecretsManager();
it.only("should not get or put a secret", async () => {
const event = {name:"event"};
// Create a mock for this instance ONLY
secretsManager.describeSecret = jest.fn().mockImplementationOnce(()=>Promise.resolve("fake-values"));
await handler(event);
expect(secretsManager.describeSecret).toHaveBeenCalled();
expect(secretsManager.describeSecret).toHaveBeenCalledWith({
SecretId: event.SecretId,
});
});
});
I have this action
Future<void> signUpAction(Store<AppState> store) async {
try {
// ...
} catch (e) {
// ..
}
}
And I dispatch it like this
store.dispatch(signUpAction);
Now, if I want to pass two paramters, how would I do that? Since there is already one parameter there.
I tried this
Future<void> signUpAction(Store<AppState> store, email, password) async {
try {
// ...
} catch (e) {
// ..
}
}
but then on dispatching, if I do
store.dispatch(signUpAction("some#email.com", "somEPa55word!"));
it says the singUpAction expects 3 parameters, so I don't know very well how to pass only these two
Thank you
The dispatch method expects a specific signature. If your method does not have exactly that signature, you can make an anonymous function on the fly that matches the signature.
In this case, since your method takes not only the store, but also the email and password:
store.dispatch((x) => signUpAction(x, "some#email.com", "somEPa55word!"));
I'm converting some existing redux code to the toolkit way. We have a lot of actions that trigger thunks (to load data from backend) but dont have a reducer. Our pattern being the load/success/fail triple. Basically only the success and fails need a reducer statement. How do I do this with the toolkit? Do I have to put in a reducer that just returns the unchanged state for the load actions?
With redux-toolkit you have a few options here...
1. Existing thunks + RTK actions
If you only need to update one slice of your store with the loaded data, you can create “success” and “fail” actions in the reducers property on that slice. Then, change your thunk to dispatch those instead of the old success/fail actions.
const slice = createSlice({
name: 'data',
initialState: {},
reducers: {
fetchDataSuccess(state, action) {
// Do something with the response
},
fetchDataError(state, action) {
// Do something with the error
}
}
}
const { fetchDataSuccess, fetchDataError } = slice.actions
export function fetchData() {
return dispatch => api.getData()
.then(response => dispatch(fetchDataSuccess(response.data)))
.catch(error => dispatch(fetchDataError(error))
}
export default slice.reducer
2. Existing thunks + extraReducers
If you don't want to refactor the existing thunk, or if the actions will be used across multiple slices, you can use the extraReducers property.
// These can also be defined in a separate file and imported
const FETCH_SUCCESS = 'data/FETCH_SUCCESS'
const FETCH_FAIL = 'data/FETCH_FAIL'
export function fetchData() {
return dispatch => api.getData()
.then(response => dispatch({ type: FETCH_SUCCESS, payload: response.data }))
.catch(error => dispatch({ type: FETCH_FAIL, payload: error }))
}
const slice = createSlice({
// ... the usual properties
extraReducers: {
[FETCH_SUCCESS](state, action) {
// Do something with the response
},
[FETCH_FAIL](state, action) {
// Do something with the error
}
}
}
3. createAsyncThunk
This approach is similar to the above, but the createAsyncThunk utility handles a lot of it for you, like catching errors, dispatching the actions at the right time, etc.
const fetchData = createAsyncThunk(
'data/fetchData',
() => api.getData().then(response => response.data)
)
const slice = createSlice({
// ... the usual properties
extraReducers: {
[fetchData.fulfilled](state, action) {
// Do something with the response
},
[fetchData.rejected](state, action) {
// Do something with action.error
}
}
}
// Components still call this like a normal function: fetchData()
export { fetchData }
export default slice.reducer
Whichever way you end up going, if you're not using the "load" action (or .pending from createAsyncThunk), you don't need to add it to either reducers or extraReducers.
I think you can simply create thunk-actions.ts (or eg. saga-actions.ts) file to keep actions that trigger data loading.
import { createAction } from '#reduxjs/toolkit';
export const fetchUserComments = createAction<{ id: string }>(
'fetchUserComments',
);
all actions that have reducer's logic will be generated by slice
Can anybody tell me if it is possible to create a deferred completable in a concat operator.
I want to fetch a session, and after this load a user with the corresponding session id.
SessionAPI.post(email: email, password: password)
UserAPI.get(id: Session.load()!.userId)
Until now I used observables with the flatMap operator.
I will now try to reproduce the same behaviour with the completables, which doesn't have flatMap operator.
Working code with observables:
SessionAPI.post(email: email, password: password)
.flatMap { (_) -> Single<Any> in
return UserAPI.get(id: Session.load()!.userId)
}
New working code with completables
SessionAPI.post(email: email, password: password)
.concat(Completable.deferred { UserAPI.get(id: Session.load()!.userId) } )
I now want to create an extension for this deferred completable, like:
SessionAPI.post(email: email, password: password)
.concatDeferred(UserAPI.get(id: Session.load()!.userId))
Current extension:
extension PrimitiveSequenceType where Self.Element == Never, Self.Trait == RxSwift.CompletableTrait {
func concatDeferred(_ second: RxSwift.Completable) -> RxSwift.Completable {
return Completable.deferred { () -> PrimitiveSequence<CompletableTrait, Never> in
return second
}
}
}
Issue: The Session.load()! in UserAPI.get is loaded and crashing before SessionAPI.post finished.
Does someone got an idea to get this extension up running?
Thanks!
I'm going to assume that the reason you want to defer your UserAPI.get(id:) is because some "magic" is happening in the background where SessionAPI.post(email:password:) is making it so Session.load() is valid.
What this tells me is that the post(email:password:) should not be completable in the first place. Rather it should return an Observable<T> where T is whatever Session.load() returns.
You can't make the code work like you want:
SessionAPI.post(email: email, password: password)
.concatDeferred(UserAPI.get(id: Session.load()!.userId))
With the above code, Session.load() will get called before SessionAPI.post(email:password:) is even called no matter what code you put in concatDeferred.
The Session.load() function must be called before concatDeferred is so that the former can pass its result into the latter.