Angular end-to-end testing tips

dzaichenko

Dmytro Zaichenko

Posted on November 26, 2020

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 watchdogs, 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

...
Enter fullscreen mode Exit fullscreen mode

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");
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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]'));
  }
}
Enter fullscreen mode Exit fullscreen mode

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");
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
    },
  });
};
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
dzaichenko
Dmytro Zaichenko

Posted on November 26, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related