Testing shared_preferences on flutter - testing

I'm writing a flutter application and I'm having this issue when writing the tests. This method is supposed to write data into TextFields and tap a button which saves this data in SharedPrefs:
testWidgets('Click on login saves the credentials',
(WidgetTester tester) async {
await tester.pumpWidget(MyApp());
await tester.enterText(find.byKey(Key('phoneInput')), 'test');
await tester.enterText(find.byKey(Key('passwordInput')), 'test');
await tester.tap(find.byIcon(Icons.lock));
SharedPreferences prefs = await SharedPreferences.getInstance();
expect(prefs.getString('phone'), 'test');
expect(prefs.getString('password'), 'test');
});
This test will fail getting the SharedPreferences instance with this error:
The following TimeoutException was thrown running a test:
TimeoutException after 0:00:03.500000: The test exceeded the timeout. It may have hung.
Consider using "addTime" to increase the timeout before expensive operations.
Update: Seems that the problem is not actually the timeout because even with 60 seconds the test is not capable of resolving the SharedPreferences instance.

You needed to mock getAll from shared_preferences (ref: https://pub.dartlang.org/packages/shared_preferences)
Here's the sample code:
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart'; // <-- needed for `MethodChannel`
void main() {
setUpAll(() {
const MethodChannel('plugins.flutter.io/shared_preferences')
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'getAll') {
return <String, dynamic>{}; // set initial values here if desired
}
return null;
});
});
testWidgets('Click on login saves the credentials',
(WidgetTester tester) async {
await tester.pumpWidget(MyApp());
await tester.enterText(find.byKey(Key('phoneInput')), 'test');
await tester.enterText(find.byKey(Key('passwordInput')), 'test');
await tester.tap(find.byIcon(Icons.lock));
SharedPreferences prefs = await SharedPreferences.getInstance();
expect(prefs.getString('phone'), 'test');
expect(prefs.getString('password'), 'test');
});
}
Original answer:
testWidgets('Click on login saves the credentials',
(WidgetTester tester) async {
final AutomatedTestWidgetsFlutterBinding binding = tester.binding;
binding.addTime(const Duration(seconds: 10)); // or longer if needed
await tester.pumpWidget(MyApp());
await tester.enterText(find.byKey(Key('phoneInput')), 'test');
await tester.enterText(find.byKey(Key('passwordInput')), 'test');
await tester.tap(find.byIcon(Icons.lock));
SharedPreferences prefs = await SharedPreferences.getInstance();
expect(prefs.getString('phone'), 'test');
expect(prefs.getString('password'), 'test');
});

You could use the provided mock setMockInitialValues
testWidgets('Click on login saves the credentials',
(WidgetTester tester) async {
await tester.pumpWidget(MyApp());
SharedPreferences.setMockInitialValues(<String, dynamic>{
'flutter.phone': '',
'flutter.password': '',
});
await tester.enterText(find.byKey(Key('phoneInput')), 'test');
await tester.enterText(find.byKey(Key('passwordInput')), 'test');
await tester.tap(find.byIcon(Icons.lock));
SharedPreferences prefs = await SharedPreferences.getInstance();
expect(prefs.getString('phone'), 'test');
expect(prefs.getString('password'), 'test');
});

As mentioned at github you can do something like this at the beginning of your test:
tester.addTime(const Duration(seconds: 10));
Here is the link and full example :
https://github.com/flutter/flutter/issues/19175
testWidgets('MyWidget', (WidgetTester tester) async {
final AutomatedTestWidgetsFlutterBinding binding = tester.binding;
binding.addTime(const Duration(seconds: 10)); // or longer if needed
await tester.pumpWidget(new MyWidget());
await tester.tap(find.text('Save'));
expect(find.text('Success'), findsOneWidget);
});

The solution provided by TruongSinh was not working for me. When I was trying to set values to SharedPreferences from the code, not tests, I was getting errors.
So here is an addition to that answer. You need to reimplement setters by yourself:
import 'package:flutter/services.dart';
void main() {
setUpAll(() {
final values = <String, dynamic>{}; // set initial values here if desired
const MethodChannel('plugins.flutter.io/shared_preferences')
.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'getAll') {
return values;
} else if (methodCall.method.startsWith("set")) {
values[methodCall.arguments["key"]] = methodCall.arguments["value"];
return true;
}
return null;
});
});
testWidgets('Test if your code works as designed', (WidgetTester tester) async {
...
});
}

Related

A call to an async function is not awaited. Use the "await" keyword in Test Cafe

I'm getting this error:
A call to an async function is not awaited. Use the "await" keyword
before actions, assertions or chains of them to ensure that they run
in the right sequence.
But I am using an await within my code, what am I doing incorrectly?
test("Accepts the cookie", async t => {
const interfaceSelect = Selector('#sp_message_iframe_617100');
await
t.switchToIframe('#sp_message_iframe_617100')
t.click(Selector('button').withText('Accept'))
t.switchToIframe('#select_region-select-module_select_30IZf')
const countrySelect = Selector('#region-select-module_select__30lZf');
t.click(Selector('button').withText('United Kingdom'))
});
Thanks,
The TestCafe's methods are chainable and return a Promise.
You need to add an await before each method call or one await before the whole chain.
test("Accepts the cookie", async t => {
const interfaceSelect = Selector('#sp_message_iframe_617100');
await t.switchToIframe('#sp_message_iframe_617100');
await t.click(Selector('button').withText('Accept'));
await t.switchToIframe('#select_region-select-module_select_30IZf');
const countrySelect = Selector('#region-select-module_select__30lZf');
await t.click(Selector('button').withText('United Kingdom'));
});
test("Accepts the cookie", async t => {
const interfaceSelect = Selector('#sp_message_iframe_617100');
await t
.switchToIframe('#sp_message_iframe_617100')
.click(Selector('button').withText('Accept'))
.switchToIframe('#select_region-select-module_select_30IZf');
const countrySelect = Selector('#region-select-module_select__30lZf');
await t.click(Selector('button').withText('United Kingdom'));
});

Sequelize, findOrCreate + findAll unexpected behavior

I'm trying to fetch data from a dog API and I want to add to my database only their temperaments. I tried using some loops and a split to isolate the data and then using
findOrCreate() to add only those who are not already in the DB, after that I use findAll()
to get that info from the DB to send it using expressJS.
The unexpected behavior comes when I go to the route that executes all this and the route
only gives about half of the temperaments (they are 124 and it displays arround 54), then when I refresh the page it shows all 124 of them. The DB gets populated with all the 124 in one go so the problem is with findAll()
This is the function that isolates the temperaments and append them to the DB:
module.exports = async () => {
const info = await getAllDogs();
info.forEach(async (element) => {
const { temperament } = element;
if (temperament) {
const eachOne = temperament.split(", ");
for (i in eachOne) {
await Temperament.findOrCreate({
where: { name: eachOne[i] },
});
}
}
});
};
And this is the code that gets executed when I hit my expressJS sv to get the info
exports.temperaments = async (req, res) => {
try {
await getTemperaments(); //this function is the above function
} catch (error) {
res.status(500).send("something gone wrong", error);
}
const temperamentsDB = await Temperament.findAll();
res.json(temperamentsDB);
};
So as you can see the last function executes the function that appends all the data to the DB and then sends it with findAll and res.json()
forEach is a synchronous method so it doesn't await a result of the async callback. You need to do for of in order to get wait for all results:
module.exports = async () => {
const info = await getAllDogs();
for (element of info) {
const { temperament } = element;
if (temperament) {
const eachOne = temperament.split(", ");
for (i in eachOne) {
await Temperament.findOrCreate({
where: { name: eachOne[i] },
});
}
}
}
};

react-native-community/asyncStorage removeItem causes program to behave weirdly

I have this little code snippet executed during the user logout.
async function logoutAction(props) {
removeUser();
props.logoutUser();
}
The function inside removeUser() is as :
export const removeUser = async () => {
try {
await AsyncStorage.removeItem(Constant.storage.user_data);
await AsyncStorage.removeItem(Constant.storage.token);
await AsyncStorage.removeItem(Constant.storage.notification_token);
return true;
} catch (exception) {
return false;
}
}
This clears user related data from local storage.
Similarly, props.logoutUser() is a reference call to reducer which sets loggedIn status to false.
I'm having this issue that if the removeUser() function is called once, the axios http requests do not enter the interceptors anymore and every request catches an error 'undefined'. If this method is removed though, everything works fine.
I can get it to working state then by removing the interceptors once, performing a request and then adding the interceptors again, which I found after hours of here and there.
My interceptors are:
export const requestInterceptor = axios.interceptors.request.use(
async config => {
const token = await getToken();
if (token != '') {
config.headers.Authorization = token;
}
console.log('axios request', config);
return config;
},
error => {
// console.warn('on request error')
return Promise.reject(error);
},
);
export const responseInterceptor = axios.interceptors.response.use(
function(response) {
console.log('axios response', response);
// console.warn('on response success', response.status)
return response;
},
async function(error) {
if (error.response.status === 401) {
//logout user
return;
}
return Promise.reject(error);
},
);
I am using the #react-native-community/AsyncStorage package for maintaining local storage. I suspect that the issue might be in the removeItem method but I'm unsure as the official docs don't contain the removeItem method, or in the interceptor which doesn't seem faulty to me anyways.
What am I doing wrong here?? Please show me some light..
Or maybe try add a await before removeUser(); ?
async function logoutAction(props) {
await removeUser();
props.logoutUser();
}
The issue was quite silly and did not even concern AsyncStorage or removeItem and as Matt Aft pointed out in the comment, it was due to the call for token in the interceptor after it had been removed while logging out. So, replacing
const token = await getToken();
if (token != '') {
config.headers.Authorization = token;
}
by
await getToken()
.then(token => {
config.headers.Authorization = token;
})
.catch(_ => {
console.log('no token');
});
in the interceptor and returning promise from the getToken method did the thing.
Thanks to Matt and 高鵬翔.

How do I split my Jest + Puppeteer tests in multiple files?

I am writing automated tests using Jest & Puppeteer for a Front-end application written in Vue.js
So far I managed to write a set of tests, but they all reside in the same file:
import puppeteer from 'puppeteer';
import faker from 'faker';
let page;
let browser;
const width = 860;
const height = 1080;
const homepage = 'http://localhost:8001/brt/';
const timeout = 1000 * 16;
beforeAll(async () => {
browser = await puppeteer.launch({
headless: false, // set to false if you want to see tests running live
slowMo: 30, // ms amount Puppeteer operations are slowed down by
args: [`--window-size=${width},${height}`],
});
page = await browser.newPage();
await page.setViewport({ width, height });
});
afterAll(() => {
browser.close();
});
describe('Homepage buttons', () => {
test('Gallery Button', async () => {
// navigate to the login view
await page.goto(homepage);
await page.waitFor(1000 * 0.5); // without this, the test gets stuck :(
await page.waitForSelector('[data-testid="navBarLoginBtn"]');
await page.click('[data-testid="navBarLoginBtn"]'),
await page.waitForSelector('[data-testid="navBarGalleryBtn"]');
await page.click('[data-testid="navBarGalleryBtn"]'),
// test: check if we got to the gallery view (by checking nr of tutorials)
await page.waitForSelector('.card-header');
const srcResultNumber = await page.$$eval('.card-header', (headers) => headers.length);
expect(srcResultNumber).toBeGreaterThan(1);
}, timeout);
});
describe('Register', () => {
const btnLoginToRegister = '#btn-login-to-register';
const btnRegister = '#btn-register';
const btnToLogin = '#btn-goto-login';
test('Register failed attempt: empty fields', async () => {
// navigate to the register form page via the login button
await page.goto(homepage);
await page.waitForSelector(navLoginBtn);
await page.click(navLoginBtn);
await page.waitForSelector(btnLoginToRegister);
await page.click(btnLoginToRegister);
// test; checking for error messages
await page.waitForSelector(btnRegister);
await page.click(btnRegister);
const errNumber = await page.$$eval('#errMessage', (err) => err.length);
expect(errNumber).toEqual(3);
}, timeout);
test('Register failed: invalid char count, email format', async () => {
// fill inputs
await page.waitForSelector('#userInput');
await page.type('#userInput', 'a');
await page.waitForSelector('#emailInput');
await page.type('#emailInput', 'a');
await page.waitForSelector('#emailInput');
await page.type('#passInput', 'a');
await page.waitForSelector(btnRegister);
await page.click(btnRegister);
// test: check if we 3 errors (one for each row), from the front end validations
const err = await page.$$eval('#errMessage', (errors) => errors.length);
expect(err).toEqual(3);
}, timeout);
test('Register: success', async () => {
await page.click('#userInput', { clickCount: 3 });
await page.type('#userInput', name1);
await page.click('#emailInput', { clickCount: 3 });
await page.type('#emailInput', email1);
await page.click('#passInput', { clickCount: 3 });
await page.type('#passInput', password1);
await page.waitForSelector(btnRegister);
await page.click(btnRegister);
// test: check if go to login link appeared
await page.waitForSelector(btnToLogin);
await page.click(btnToLogin);
// await Promise.all([
// page.click(btnToLogin),
// page.waitForNavigation(),
// ]);
}, timeout);
test('Register failed: email already taken', async () => {
// navigate back to the register form
await page.waitForSelector(btnLoginToRegister);
await page.click(btnLoginToRegister);
await page.click('#userInput');
await page.type('#userInput', name2);
await page.click('#emailInput');
await page.type('#emailInput', email1); // <- existing email
await page.click('#passInput');
await page.type('#passInput', password2);
await page.click(btnRegister);
const err = await page.$eval('#errMessage', (e) => e.innerHTML);
expect(err).toEqual('Email already taken');
}, timeout);
});
I would like to be able to have a single test file that does the beforeAll and afterAll stuff, and each test suite: HomepageButtons, Register, etc. to reside in it's own test file. How would I be able to achieve this?
I've tried splitting tets into:
testsUtils.js that would contain the beforeAll and afterAll hooks and code but it doesn't guarantee that it runs when it needs: the beforeAll code to fire before all other test files and the afterAll code to fire after all the test files finished.
Sorry, I'd rather comment on your question, but I don't have reputation for that. Anyway, I think that you are looking for something like a "global beforeAll" and "global afterAll" hooks, right? Jest has it alread. It's called "globalSetup" and "globalTeardown".
Take a look at globalSetup. Excerpt:
This option allows the use of a custom global setup module which
exports an async function that is triggered once before all test
suites.
The Global Teardown one goes the same.
I think you'll have a headache trying to get a reference to the page or browser in globalSetup/globalTeardown and I confess that I never try this. Maybe the answer for that problem (if you have it) is on this page, under "Custom example without jest-puppeteer preset section.
Also there is a repo that tries to facilitate Jest + Puppeteer integration. Maybe you find it util: repo.
Good luck. :)

How to reuse puppeteer tests

I'm currently working with Puppeteer and Jest for end-to-end testing, for my tests to work I always need to run a login tests, but I don't know and haven't been able to find out how to export my tests so I can reuse them.
To conclude: I'm looking for a way to reuse all of my tests inside the describe by exporting them to a different file and reusing them in a beforeAll in the new files.
The complete set of login tests is below:
describe("homepage and login tests", homepageTests = () => {
test("front page loads", async (done) => {
await thePage.goto('http://localhost:3000');
expect(thePage).toBeDefined();
done();
});
test("Login button is present", async (done) => {
theLoginButton = await thePage.$("#login-button");
expect(theLoginButton).toBeDefined();
done();
})
test("Login works", async (done) => {
//the following code runs inside the popup
await theBrowser.on('targetcreated', async (target) => {
const thePopupPage = await target.page();
if (thePopupPage === null) return;
//get the input fields
const usernameField = await thePopupPage.waitFor('input[name=login]');
const passwordField = await thePopupPage.waitFor("input[name=password]");
const submitButton = await thePopupPage.waitFor('input[name=commit]');
//validate input fields
expect(usernameField).not.toBeNull();
expect(passwordField).not.toBeNull();
expect(submitButton).not.toBeNull();
//typing and clicking
await thePopupPage.waitFor(300)
await usernameField.type("USER");
await passwordField.type("PASSWORD");
await submitButton.click();
done();
})
try {
//wait for login button on homepage
theLoginButton = await thePage.waitFor('#login-button');
expect(theLoginButton).toBeDefined();
//click on login
await thePage.waitFor(200);
await theLoginButton.click();
} catch (e) { console.log(e) }
})
test("Arrive on new page after login", async () => {
//resultsButton is only shown for logged in users.
const resultsButton = await thePage.$("#resultsButton");
expect(resultsButton).toBeDefined();
})
Create a separate file name test.js
//test.js
export async function fn1(args){
// your commands
}
//file.test.js
import {fn1} from 'test.js'
describe('test 1 ', () => {
test("test", async () => {
try {
await fn1(args);
} catch (err) {
console.log('There are some unexpected errors: ' + err);
}
},5000);
});
I have the same issue and the above method will work out for you.