📜 ⬆️ ⬇️

Service control panel. Part 3. Reconnaissance

In the previous part I finished the story about the API and the interface with the front end. In this article I will talk about the frontend itself and start with a topic that usually opens closer to the end. Testing.



File organization


To begin with, the structure of the project formed by angular-cli. As unnecessary, some of the files were filtered.


Unit testing in Angular is based on Jasmine , the utilities of Angular and Karma itself . The description of these packages is given on the official website , so I will not duplicate it. In short, Jasmine is a framework that provides the necessary functionality for basic tests, Angular utilities allow you to use a test environment, Karma is used to run tests.
')


The test files themselves are named with the addition of the spec before the extension. According to this pattern: * .spec.ts Karma will find them. There is no consensus about the location of the tests. Allowed location "closer" to the object of testing and a separate directory for all tests of the application. The Angular team gives the advantages of the location of the tests next to the object:


I opted for the option proposed by angular-cli - tests are located next to the object.

Karma setting


The entry point for testing the Angular application is the src / test.ts file, which imports all the necessary packages, collects the test files using the * .spec.ts template and starts the test execution system, in our case Karma. The file itself after creating the project looks like this:

// This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/dist/long-stack-trace-zone'; import 'zone.js/dist/proxy.js'; import 'zone.js/dist/sync-test'; import 'zone.js/dist/jasmine-patch'; import 'zone.js/dist/async-test'; import 'zone.js/dist/fake-async-test'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. declare const __karma__: any; declare const require: any; // Prevent Karma from running prematurely. __karma__.loaded = function () {}; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting() ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. context.keys().map(context); // Finally, start Karma to run the tests. __karma__.start(); 

Here I want to draw attention to the problem that I had to face. The order of imports is important and you should not change it, otherwise you will be able to meet errors whose roots are extremely implicit. Therefore, I recommend adding third-party packages to the end of the import list and adding the / * tslint: disable * / flag to the beginning of the file to disable the linter, or exclude this file in its configuration, because tslint might consider the existing order incorrect and correct.

Before writing tests, you need to configure Karma, which will run them. The initial configuration is as follows:

 // Karma configuration file, see link for more information // https://karma-runner.imtqy.com/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular/cli'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), require('@angular/cli/plugins/karma'), require('karma-mocha-reporter'), ], client:{ clearContext: false // leave Jasmine Spec Runner output visible in browser }, coverageIstanbulReporter: { reports: [ 'html', 'lcovonly' ], fixWebpackSourcePaths: true }, angularCli: { environment: 'dev' }, reporters: ['mocha'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false }); }; 

Since the description of all possible Karma settings is a separate topic for discussion, I’ll dwell only on those that we use:


So, the preparatory stage is over, you can run the tests. For a project generated using angular-cli, use the ng test command. It has several parameters , among which I would like to mention - sourcemap. As the name suggests, this parameter includes sourcemaps in the tests. But this is the error 'XMLHttpRequest: failed to load'. Setting false for this parameter solves this problem.

Component testing


The first and most important testing utility for the Angular component is TestBed . Her idea is to create a separate module that is identical to the component module, but is created in a test environment and is isolated from the application.

To create a TestBed, the configureTestingModule method is called, to which the metadata object is passed. This method should be called asynchronously before each test so that the component is in the initial state. For this there is a method beforeEach (async () => {});

 beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], }).compileComponents(); })); 

As you can see from the example, we declare an array of components for testing (in this case, only one). In the future, it is possible to add more components, import the necessary modules for work, directives, pipes. The configureTestingModule method returns a TestBed class, which allows calling static methods such as compileComponents. This method asynchronously compiles all the components of the module, translates the template and styles into “inline”. After that, we can synchronously get the component instances using the TestBed.createComponent () method. This method returns an instance of ComponentFixture, which gives access directly to the component and its DOM through a DebugElement.

The summary file of the simplest test component
 import { TestBed, async } from '@angular/core/testing'; import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], }).compileComponents(); })); it('should create the app', async(() => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.debugElement.componentInstance; expect(app).toBeTruthy(); })); it(`should have as title 'app'`, async(() => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.debugElement.componentInstance; expect(app.title).toEqual('app'); })); it('should render title in a h1 tag', async(() => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.debugElement.nativeElement; expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); })); }); 


Component code
 import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app'; } 


Component template
 <div style="text-align:center"> <h1> Welcome to {{title}}! </h1> </div> 


Testing a component with addiction


Developers emphasize that you should not use real services and add stubs to the tests.

Component code


 import {Component} from '@angular/core'; import {SomeServiceService} from "./some-service.service"; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app'; name: string; constructor(private someService: SomeServiceService) { this.name = this.someService.getName(); } } 

Test code:


 import { TestBed, async } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { SomeServiceService } from "./some-service.service"; const SomeServiceStub = { getName: () => 'some name' }; describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], providers: [ //   { provide: SomeServiceService, useValue: SomeServiceStub } ] }).compileComponents(); })); it('token should test component with dependency', () => { //   root-injector //       ,        beforeEach const someService = TestBed.get(SomeServiceService); expect(someService.getName()).toEqual('some name'); }); // ,    inject,   ,    root-injector. //  ,      it('token should test component with dependency', inject(([SomeServiceService], (someService: SomeServiceService)) => { const someService = TestBed.get(SomeServiceService); expect(someService.getName()).toEqual('some name'); })); }); 

In this case, I use an object that repeats the behavior of SomeService, that is, contains the getName method. To use it instead of a real service, you need to add an object with the provide property to the providers array, specifying the service as the value and useValue with the stub value.

Since we use API based on swagger, there is no need to write services to communicate with the server. We can only call the methods and process the result. And here we can afford to refuse to implement the methods and get the service through the injector.
This is done through the use of MockBackend. In short, we are replacing not the service, but the backend itself.

Component code
 @Component({ selector: 'app-login', templateUrl: 'login.component.html', styleUrls: ['login.component.less'] }) export class LoginComponent implements OnInit { loading: boolean; loginForm: FormGroup; error: any; constructor(private router: Router, private authService: UserService, private fb: FormBuilder, private ref: ChangeDetectorRef) { } ngOnInit() { this.loading = false; this.loginForm = this.fb.group({ login: [''], password: [''] }); } get login() { return this.loginForm.get('login'); } get password() { return this.loginForm.get('password'); } submit() { this.loading = true; this.submitted = true; //     const data = { 'login': this.loginForm.controls['login'].value, 'password': this.loginForm.controls['password'].value }; //    //    -    /home/main //      400  401 -    this.authService.signIn(data).subscribe( resp => this.router.navigateByUrl('/home/main'), error => { this.loading = false; error.status === 401 || error.status === 400 ? this.error = {errorText: error.title} : ''; this.ref.detectChanges(); } ); } } 


Test code
 class MockError extends Response implements Error { name: any; message: any; } describe('LoginComponent', () => { let component: LoginComponent; let fixture: ComponentFixture<LoginComponent>; let mockBackend: MockBackend; const authUrl = 'login_url'; const testUser = { login: 'test', password: 'test' }; const successResponse = new Response( new ResponseOptions({ status: 200, body: '' }) ); const errorResponse = new MockError( new ResponseOptions({ type: ResponseType.Error, status: 401, body: JSON.stringify({ status: 401, title: 'Unauthorized', } ) }) ); beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [LoginComponent], imports: [ FormsModule, HttpModule, RouterTestingModule.withRoutes([]), ], providers: [ AuthService, {provide: XHRBackend, useClass: MockBackend} ], }).compileComponents(); mockBackend = TestBed.get(XHRBackend); mockBackend.connections.subscribe((connection: MockConnection) => { if (connection.request.method === RequestMethod.Post) { expect(connection.request.url).toEqual(authUrl); (connection.request.getBody() === JSON.stringify(testUser)) ? connection.mockRespond(successResponse) : connection.mockError(errorResponse); } else { connection.mockError(errorResponse); } }); })); beforeEach(() => { fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should login correctly', inject([Router], (router: Router) => { const spy = spyOn(router, 'navigateByUrl'); const login = component.loginForm.controls['login']; const password = component.loginForm.controls['password']; login.setValue('test'); password.setValue('test'); fixture.detectChanges(); component.submit(); fixture.detectChanges(); const navArgs = spy.calls.first().args[0]; expect(navArgs).toEqual('/home/main'); })); it('should fail login', () => { const login = component.loginForm.controls['login']; const password = component.loginForm.controls['password']; const errorResponse = { errorText: 'Unauthorized' }; login.setValue('testad'); password.setValue('testad'); fixture.detectChanges(); component.submit(); fixture.detectChanges(); expect(component.error).toEqual(errorResponse); }); }); 


We have a component that needs to take a login and password from the form, send it to a certain url and either follow the 'home / main' link or process and display an error.

In order not to use the stub for the service and not to implement its methods, we use MockBackend, which will process requests and return predefined answers. To do this, we first declare MockBackend, specify the url, which we expect and define the data that should be in the request:

 let mockBackend: MockBackend; const authUrl = 'login_url'; const testUser = { login: 'test', password: 'test' }; 

Next, we define what the answers will look like. Suppose that a successful request returns an empty response, and an invalid one returns a status 401 with the name of the error.

 const successResponse = new Response( new ResponseOptions({ status: 200, body: '' }) ); const errorResponse = new MockError( new ResponseOptions({ type: ResponseType.Error, status: 401, body: JSON.stringify({ status: 401, title: 'Unauthorized', } ) }) ); 

Next, after configuring the test module, we get a MockBackend and subscribe to the connections. We expect the POST method and the matching url of the request to what we indicated above. Also, we check that the request body matches what was expected.

 mockBackend = TestBed.get(XHRBackend); mockBackend.connections.subscribe((connection: MockConnection) => { if (connection.request.method === RequestMethod.Post) { expect(connection.request.url).toEqual(authUrl); (connection.request.getBody() === JSON.stringify(testUser)) ? connection.mockRespond(successResponse) : connection.mockError(errorResponse); } else { connection.mockError(errorResponse); } }); 

All that remains to be done in the test is to get a router (since we have a transition when the request is successful), fill in the login data and check that the transition was completed:

 it('should login correctly', inject([Router], (router: Router) => { const spy = spyOn(router, 'navigateByUrl'); const login = component.loginForm.controls['login']; const password = component.loginForm.controls['password']; login.setValue('test'); password.setValue('test'); fixture.detectChanges(); component.submit(); fixture.detectChanges(); const navArgs = spy.calls.first().args[0]; expect(navArgs).toEqual('/home/main'); })); 

Component testing with input and output


As you know, components can exchange data through properties with the decorators @Input and @Output . For example, like this:

Parent Component:


 import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: '<child-comp [userName]="name" [userSurname]="surname"></child-comp>' }) export class ParentComponent { name = 'Some name'; surname = 'Some surname'; } 

ChildComponent:


 import { Input, Component} from '@angular/core'; @Component({ selector: 'child-comp', template: `<p> : {{userName}}</p> <p> : {{userSurname}}</p>` }) export class ChildComponent{ @Input() userName: string; @Input() userSurname:string; } 

In order for the Parent component test to start correctly, you must declare a child-component in the declarations array.

parent.component.spec.ts


 import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {ParentComponent} from './parent.component'; import {ChildComponent} from '../child/child.component'; describe('ParentComponent', () => { let component: ParentComponent; let fixture: ComponentFixture<ParentComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ParentComponent, ChildComponent] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(ParentComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); 

And for the Child component, you must pass the input parameters. The easiest way is to manually specify their values ​​after creating the component:

child.component.spec.ts


 import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ChildComponent } from './child.component'; describe('ChildComponent', () => { let component: ChildComponent; let fixture: ComponentFixture<ChildComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ ChildComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(ChildComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); component.userName = 'some name'; expect(component.userName).toEqual('some name'); }); }); 

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


All Articles