In CodeceptJS we encourage users to follow semantic elements on page while writing tests. Instead of CSS/XPath locators try to stick to visible keywords on page.
Take a look into the next example:
// it's fine but... I.click({css: 'nav.user .user-login'}); // can be better I.click('Login', 'nav.user');
If we replace raw CSS selector with a button title we can improve readability of such test. Even if the text on the button changes, it's much easier to update it.
If your code goes beyond using
I
object or page objects, you are probably doing something wrong.
When it's hard to match text to element we recommend using locator builder. It allows to build complex locators via fluent API. So if you want to click an element which is not a button or a link and use its text you can use locate()
to build a readable locator:
// clicks element <span class="button">Click me</span> I.click(locate('.button').withText('Click me'));
To write simpler and effective tests we encourage to use short cuts. Make test be focused on one feature and try to simplify everything that is not related directly to test.
Make test as simple as:
Scenario('editing a metric', async ({ I, loginAs, metricPage }) => { // login via autoLogin loginAs('admin'); // create data with ApiDataFactory const metric = await I.have('metric', { type: 'memory', duration: 'day' }) // use page object to open a page metricPage.open(metric.id); I.click('Edit'); I.see('Editing Metric'); // using a custom step I.selectFromDropdown('duration', 'week'); I.click('Save'); I.see('Duration: Week', '.summary'); });
{ css: 'button' }
or { xpath: '//button' }
. We call them strict locators. Those locators will be faster but less readable.data-test
or data-qa
. Use customLocator
plugin to easily add them to tests.When a project is growing and more and more tests are required, it's time to think about reusing test code across the tests. Some common actions should be moved from tests to other files so to be accessible from different tests.
Here is a recommended strategy what to store where:
custom_steps.js
file). Such actions like login
, using site-wide common controls, like drop-downs, rich text editors, calendars.Learn more about different refactoring options
However, it's recommended to not overengineer and keep tests simple. If a test code doesn't require reusage at this point it should not be transformed to use page objects.
class CheckoutForm { fillBillingInformation(data = {}) { // take data in a flexible format // iterate over fields to fill them all for (let key of Object.keys(data)) { I.fillField(key, data[key]); // like this one } } } module.exports = new CheckoutForm(); module.exports.CheckoutForm = CheckoutForm; // for inheritance
class DropDownComponent { selectFirstItem(locator) { I.click(locator); I.click('#dropdown-items li'); } selectItemByName(locator, name) { I.click(locator); I.click(locate('li').withText(name), '#dropdown-items'); } }
const { I } = inject(); /** * Calendar works */ class DatePicker { selectToday(locator) { I.click(locator); I.click('.currentDate', '.date-picker'); } selectInNextMonth(locator, date = '15') { I.click(locator); I.click('show next month', '.date-picker') I.click(date, '.date-picker') } } module.exports = new DatePicker; module.exports.DatePicker = DatePicker; // for inheritance
codecept.conf.js
- default onecodecept.ci.conf.js
- for CIcodecept.windows.conf.js
- for Windows, etc.env
files and dotenv package to load sensitive datarequire('dotenv').config({ path: '.env' });
config
dir// inside config/components.js module.exports = { DatePicker: "./components/datePicker", Dropdown: "./components/dropdown", }
include them like this:
include: { I: './steps_file', ...require('./config/pages'), // require POs and DOs for module ...require('./config/components'), // require all components },
config/plugins.js
and export themconfig/plugins.js
and export them// inside codecept conf file bootstrap: () => { codeceptjs.container.append({ testUser: { email: '[email protected]', password: '123456' } }); } // now `testUser` can be injected into a test
include: { // ... testData: './config/testData' }
const faker = require('faker'); const { I } = inject(); const { output } = require('codeceptjs'); class InterfaceData { async getLanguages() { const { data } = await I.sendGetRequest('/api/languages'); const { records } = data; output.debug(`Languages ${records.map(r => r.language)}`); return records; } async getUsername() { return faker.user.name(); } } module.exports = new InterfaceData;
© 2015 DavertMik <[email protected]> (http://codegyre.com)
Licensed under the MIT License.
https://codecept.io/best/