Angular end-to-end testing tips
We all love to write end-to-end specs, don’t we? The reason is that these scenarios act as watch dogs, making refactoring safer and sometimes being the one and only feature documentation existing in the codebase.
The only downside is that sometimes it takes ages to have a proper data setup, resolve CSS classes dependencies and constant CSS/HTML changes. Readability and maintainability are not always perfect too. Well, we have a few simple techniques that can help you overcome most of the issues described above. Written for end-to-end Protractor specs, they can be easily used with a testing framework you prefer.
Let’s check a simple example of markup
...
I'm an awesome label
...
with related specs
describe('Awesome page', () => {
beforeAll(() => {
browser.driver.get("http://mysite.com/awesome");
});
describe('Awesome block', () => {
const block = element(by.css('.awesome-block'));
const label = block.element(by.css('.utility-class'));
it('has awesome label', () => {
expect(label.getText()).toEqual("I'm an awesome label");
});
});
});
and try to improve them.
Separate test specific attributes
If you have engineers working separately with CSS/HTML and Angular/JS components then you have probably faced an issue that markup changes are not safe in terms of spec dependencies.
Front-end engineer can accidentally break specs by just changing the utility-class name or applying different class according to CSS changes. Although this issue can be avoided by checking end-to-end specs selectors whenever a markup change is applied, that’s not very convenient. An alternative solution would be having proper stable semantic classes on every tested element, but that’s just too ideal
The other option is to have a special attribute that is used ONLY by testing framework:
I'm an awesome label
It looks like we are having obsolete attributes all around our markup. Although using this technique we have obtained a number of benefits:
Every tested element has a separate meaningful name
Markup changes are much easier and "safer"
Specs do not dependent on CSS changes
Page/Component objects
When writing end-to-end tests, a common pattern is to use page objects. It makes easier to maintain and reuse spec examples. Let’s define simple page objects for our specs:
class PageObject {
constructor(public finder: ElementFinder) { }
protected get element() {
return this.finder.element.bind(this.finder);
}
protected getChild(locator: string) {
return this.element(by.css(locator));
}
}
class AwesomeBlock extends PageObject {
get awesomeLabel() {
return this.getChild('[data-test=awesome-label]');
}
}
class AwesomePage extends PageObject {
visit() {
browser.driver.get("http://mysite.com/awesome");
}
get awesomeBlock() {
return new AwesomeBlock(this.getChild('[data-test=awesome-block]'));
}
}
Test examples will now look like this:
const page = new AwesomePage(element(by.css("body")));
describe('Awesome page', () => {
beforeAll(() => {
page.visit();
});
describe('Awesome block', () => {
const awesomeBlock = page.awesomeBlock;
it('has awesome label', () => {
expect(awesomeBlock.awesomeLabel.getText()).toEqual("I'm an awesome label");
});
});
});
Much cleaner, no CSS selectors in examples but can we improve this even more? Sure! With a common test-specific attribute on every testable element and TypeScript decorators page objects can look a bit fancier:
class AwesomeBlock extends PageObject {
@hasOne awesomeLabel;
}
class AwesomePage extends PageObject {
visit() {
browser.driver.get("http://mysite.com/awesome");
}
@hasOne awesomeBlock: AwesomeBlock;
with decorator defined as:
export const hasOne = (target: any, propertyKey: string) => {
Object.defineProperty(target, propertyKey, {
enumerable: true,
configurable: true,
get: function () {
const child = this.getChild(`[data-test=${_.kebabCase(propertyKey)}]`);
const PropertyClass = Reflect.getOwnMetadata("design:type", target, propertyKey);
return new PropertyClass(child);
},
});
};
Now we have reusable spec examples that are not dependent on CSS changes and a nice DSL to define Page/Component classes.
The insights and code samples were made by Railsware engineering team