Angular: Integration Testing (Shallow testing)



When the application grows, or it is very important for us that it works correctly with any refactoring, we begin to think about unit or e2e testing.

For several years of working with Angular - applications in the corporate segment, having caught many problems when refactoring and creating new functionality, tests do not seem like a waste of time, but the key to stability in the hands of programmers.

Next, let's try to understand how to test the basic application on Angular and touch on a little theory.

Let's start with the types of testing that application developers can do (in our case, frontend).



(Martin Fowler and Google Testing Blog)

Angular has such methods out of the box.

Let's take a closer look at the testing pyramid using the example of Angular applications.

Unit testing - isolated testing of class methods. We check the operation of the method, passing various combinations of parameters to the input, and compare the result with the expected one.

For example, we have a component in which hex strings are converted to rgb, this information is displayed to the user, and also sent to the rest server.

If we start looking towards unit tests, then we can write many tests for each method in the component class.

If we have a method rgbToHex (hex: string) => string, then to test a method with such a signature we need to do the following: expect (rgbToHex ('777')). ToBe ('rgb (119, 119, 119)') .

It seems great, but we have a problem due to the large number of functions that need to be covered, and we are too lazy to write tests. In addition, even writing unit tests for each method in a component, we do not guarantee the correctness of their joint work.

For example, having made and tested the table output component, we can call it in the β€œcalculator” component, but accidentally specify the wrong binding. Then the data will not fall into the tables, and fully working components will not work correctly.

image

Integration testing- testing a bunch of several components. From this stage we begin to test not just the class methods, but also their binding to html, i.e. click on the elements inside the component. Shallow testing is often found in Angular notation , which is essentially integration testing.

At Shallow testing we will dwell in more detail below.

E2E (end-to-end) testing - a way to test the application completely to solve the problems of unit tests.

With this approach, we write test scripts for a fully rendered application, i.e. all components and services are assembled together, and we reproduce user actions.
This is very cool, but we may have the problem of dynamically changing stubs (usually a static json server on node.js), and to play different situations, we may need different data from the emulated server.

For example: we have a page with a list of users, one of the components of this panel is pagination. It should behave differently with different numbers of users. But if the data on the emulated server is set via json, then the number of users will always be one, and we will not be able to check all cases with a different number of pages.

As a result, we can conclude that if we want to flexibly change stubs and test not by methods, but by logical units of the interface, we need to use something in between e2e and unit-tests. This is where shallow testing (integration testing) comes to the rescue.

Glossary:

  • Mocks are objects for simulating responses in various use-cases.
  • Stubs are the most stupid stubs without logic (you can read Martin).
  • Karma is a test runner built into angular (often advised to use jest instead, but not about that today).
  • Jasmine is a framework for describing tests in the form of specs (see below).
  • spec - test file extension (specification description in BDD style).
  • it is the name of the methods for tests in Jasmine.
  • xit is the name of the methods that will not run.
  • fit - if there are methods with the same name in specs, only they will be launched.

Shallow testing for Angular, as well as for other frameworks, is a unit-test approach, when a component of size large enough is rendered so that it can exist as a separate unit of ui with its own functionality.

For example, we have a component for converting hex -> rgb. We can render only this component, generate stubs for different situations, execute possible use-cases for this component from the point of view of end users and check the operation of the component.

Let's try to figure out an example (repository).

Prepare the class for accessing the components of the component in accordance with PageObject and add a helper to the root of the project.

Helper - will help to search for elements in the components that have been selected for rendering. This way we can make life easier if we use Angular Material: then elements of type select will create a list with option in a separate block, and searching for these elements can lead to boilerplates, and a wrapper in the form of a helper can help.

export class PageObjectBase {

  constructor(private root: HTMLDivElement) { }
  //    input  
  _inputValue(cssSelector: string, value: string) { 
    if (value) {
      this.root.querySelector<HTMLInputElement>(cssSelector).value = value;
      this.root.querySelector<HTMLInputElement>(cssSelector).dispatchEvent(new Event('input'));
    }
    else {
      return this.root.querySelector<HTMLInputElement>(cssSelector).value
    }
  }
  //          
  _buttonClick(cssSelector: string) {
    this.root.querySelector<HTMLButtonElement>(cssSelector).dispatchEvent(new Event('click'));
  }

}

PageObject is a popular automated testing template. Used to simplify support for written tests. If we change the UI, then we don’t have to rewrite the tests, just change the element selectors.


export class ConverterFromHexPageObject extends PageObjectBase {

  constructor(root: HTMLDivElement) {
    super(root)
  }

  hex(text?: string) {
    return this._inputValue('.input-hex', text);
  }

  rgb(text?: string) {
    return this._inputValue('.input-rgb', text);
  }

  clear() {
    this._buttonClick('.btn-clear')
  }

  calc() {
    this._buttonClick('.btn-calc')
  }

}

and the tests themselves:


// ,        api   
const urlToSave = 'http://localhost:4200/save-hex';


//  -   
describe('ConverterFromHexComponent', () => {
  let component: ConverterFromHexComponent;
  let fixture: ComponentFixture<ConverterFromHexComponent>;
  let page: ConverterFromHexPageObject;
  let httpTestingController: HttpTestingController;


  //      ,   
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ConverterModule, HttpClientTestingModule]
    })
      .compileComponents();
    httpTestingController = TestBed.get(HttpTestingController);
  }));


  //  ,   
  beforeEach(() => {
    fixture = TestBed.createComponent(ConverterFromHexComponent);
    component = fixture.componentInstance;
    page = new ConverterFromHexPageObject(fixture.nativeElement);
    fixture.detectChanges();
  });


  //    ,     http 
  afterEach(() => {
    httpTestingController.verify();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should clear', async () => {
    page.hex('112233'); //  input  
    expect(page.hex()).toBe('112233'); // ,    
    await page.clear(); //      
    fixture.detectChanges(); //    
    expect(page.hex()).toBe(''); // ,   clear 
  });

  it('should convert', async () => {
    page.hex('123123');
    expect(page.rgb()).toBe('');
    page.calc();
    const req = httpTestingController.expectOne(urlToSave);
    expect(req.request.method).toEqual('POST');
    expect(req.request.body.hex).toEqual('123123');
    req.flush({});
    await fixture.detectChanges();
    expect(page.rgb()).toBe('rgb(18, 49, 35)');
  });

  it('should convert three-digit hex', async () => {
    page.hex('567');
    expect(page.rgb()).toBe('');
    page.calc();
    const req = httpTestingController.expectOne(urlToSave);
    expect(req.request.method).toEqual('POST');
    req.flush({});
    await fixture.detectChanges();
    expect(page.rgb()).toBe('rgb(85, 102, 119)');
  });

  it('rgb should be empty when entered incorrectly hex', async () => {
    page.hex('qw123we');
    page.calc();
    const req = httpTestingController.expectNone(urlToSave);
    await fixture.detectChanges();
    expect(page.rgb()).toBe('');
  });

});

Everything seems to be simple, but here are some interesting notes for Angular Shallow testing:

  • Always check that there are no unreserved http requests, this will help you to understand exactly if there are any unnecessary requests for our functionality.
  • PageObject , .
  • json , .
  • , .
  • .
  • .
  • , session, local, cookie ( ).
  • fake.js.
  • .




Source: https://habr.com/ru/post/undefined/


All Articles