각도 응용 프로그램에서 의존 관계를 위조하다

실험 도구.표지 사진은 픽셀스rawpixel.com에서 찍었다.
원시 출판일자: 2019-05-07.
Angular의 의존 주입 시스템의 강력한 기능을 이용하여 우리는 특정한 용례를 위조할 수 있다.이것은 자동화 테스트에 매우 유용하지만 본고에서 우리는 이를 수동 테스트에 사용하는 방법을 연구할 것이다.
""에서 Internet Explorer 11 기용 배너 구성 요소를 만들고 테스트 키트를 추가했습니다."실제 InternetExplorer 11 브라우저에서는 테스트되지 않았습니다.
우리의 생활을 더욱 가볍게 하기 위해서, 우리는 브라우저 위조 구성 요소를 만들 것입니다. 이 구성 요소는 개발 모드에서만 사용할 수 있습니다. 이것은 사용자 정의 구조 명령 덕분입니다.구성 요소 템플릿에서 흔히 볼 수 있는 문자열을 사용할 수 있도록 텍스트 파이프를 추가합니다.

브라우저 환경 에뮬레이션


비록 우리는 항상 실제 브라우저 목표에서 테스트를 진행해야 하지만 (이 예는 인터넷 Explorer 11) 개발 과정에서 브라우저를 선택하지 않고 다른 브라우저 환경을 편리하게 모의할 수 있기를 희망할 수 있습니다.
// user-agent.token.ts
import { InjectionToken } from '@angular/core';

export const userAgentToken: InjectionToken<string> =
  new InjectionToken('User agent string', {
    factory: (): string => navigator.userAgent,
    providedIn: 'root',
  });
// is-internet-explorer-11.token.ts
import { inject, InjectionToken } from '@angular/core';

import { userAgentToken } from './user-agent.token';

export const isInternetExplorer11Token: InjectionToken<boolean> =
  new InjectionToken('Internet Explorer 11 flag', {
    factory: (): boolean =>
      /Trident\/7\.0.+rv:11\.0/.test(inject(userAgentToken)),
    providedIn: 'root',
  });
<!-- internet-explorer-11-banner.component.html -->
<aside *ngIf="isBannerVisible">
  Sorry, we will not continue to support Internet Explorer 11.<br />
  Please upgrade to Microsoft Edge.<br />

  <button (click)="onDismiss()">
    Dismiss
  </button>
</aside>
// internet-explorer-11-banner.component.ts
import { Component, Inject } from '@angular/core';

import { isInternetExplorer11Token } from './is-internet-explorer-11.token';

@Component({
  selector: 'internet-explorer-11-banner',
  templateUrl: './internet-explorer-11-banner.component.html',
})
export class InternetExplorer11BannerComponent {
  private isDismissed = false;

  get isBannerVisible() {
    return this.isInternetExplorer11 && !this.isDismissed;
  }

  constructor(
    @Inject(isInternetExplorer11Token) private isInternetExplorer11: boolean,
  ) {}

  onDismiss() {
    this.isDismissed = true;
  }
}
원본 값 의존 관계를 가진 버려진 현수막.
현재는 배너 구성 요소를 사용하지 않고 isInternetExplorer11Token에 의존하고 있습니다.의존항을 동적으로 다른 값으로 바꾸려면 조건부로 삽입된 조상 구성 요소나 명령으로 주입기 체인을 차단해야 한다.

클래스 기반 서비스 동적 대체 의존항 사용하기


모든 모듈 주입기는 사용자 대리 영패 공장에 대해 한 번만 평가를 하고, 만약 조상 부품이나 명령이 제공한 요소 주입기에서 교체가 없다면, 우리는 다른 기술을 사용하여 의존 관계를 위조해야 한다.우리는 클래스 기반 서비스 의존으로 의존을 바꾸고 영패 의존을 주입할 것이다.
// internet-explorer-11-banner.component.ts
import { Component } from '@angular/core';

import { InternetExplorerService } from './internet-explorer.service';

@Component({
  selector: 'internet-explorer-11-banner',
  templateUrl: './internet-explorer-11-banner.component.html',
})
export class InternetExplorer11BannerComponent {
  private isDismissed = false;

  get isBannerVisible() {
    return this.internetExplorer.isInternetExplorer11State && !this.isDismissed;
  }

  constructor(
    private internetExplorer: InternetExplorerService,
  ) {}

  onDismiss() {
    this.isDismissed = true;
  }
}
// internet-explorer-service.ts
import { Inject, Injectable } from '@angular/core';

import { userAgentToken } from './user-agent.token';

@Injectable({
  providedIn: 'root',
})
export class InternetExplorerService {
  get isInternetExplorer11State(): boolean {
    return this.isInternetExplorer11(this.userAgent);
  }

  constructor(
    @Inject(userAgentToken) private userAgent: string,
  ) {}

  isInternetExplorer11(userAgent: string): boolean {
    return /Trident\/7\.0.+rv:11\.0/.test(userAgent);
  }
}
Internet Explorer 11 테스트를 서비스로 추출합니다.
우선, Internet Explorer 11은 의존항 주입 영패에서 새로 만든 InternetExplorerService 클래스로 추출합니다.Internet Explorer 11 검사 토큰은 현재 사용자 에이전트를 기반으로 가치를 평가할 때 서비스에 위임됩니다.
이 때 프로그램은 여전히 작업하고 있어야 한다.불행히도, 우리는 테스트 세트를 파괴했기 때문에, 우리는 그것을 인터넷 익스플로러 서비스로 재편성했다.
// internet-explorer-11-detection.spec.ts
import { TestBed } from '@angular/core/testing';

import { InternetExplorerService } from './internet-explorer.service';
import { FakeUserAgent } from './fake-user-agent';

describe('Internet Explorer 11 detection', () => {
  function setup({ userAgent }: { userAgent: string }) {
    const service: InternetExplorerService =
      TestBed.get(InternetExplorerService);

    return {
      isInternetExplorer11: service.isInternetExplorer11(userAgent),
    };
  }

  const nonInternetExplorerUserAgents: ReadonlyArray<string> =
    Object.entries(FakeUserAgent)
      .filter(([browser]) =>
        !browser.toLowerCase().includes('internetexplorer'))
      .map(([_browser, userAgent]) => userAgent);

  it('accepts an Internet Explorer 11 user agent', () => {
    const { isInternetExplorer11 } = setup({
      userAgent: FakeUserAgent.InternetExplorer11,
    });

    expect(isInternetExplorer11).toBe(true);
  });

  it('rejects an Internet Explorer 10 user agent', () => {
    const { isInternetExplorer11 } = setup({
      userAgent: FakeUserAgent.InternetExplorer10,
    });

    expect(isInternetExplorer11).toBe(false);
  });

  it('rejects other user agents', () => {
    nonInternetExplorerUserAgents.forEach(userAgent => {
      const { isInternetExplorer11 } = setup({ userAgent });

      expect(isInternetExplorer11).toBe(
        false,
        `Expected to reject user agent: "${userAgent}"`);
    });
  });
});
Internet Explorer 11 테스트 키트는 Internet Explorer 서비스를 사용하도록 재구성되었습니다.
앞에서 말한 바와 같이, 우리는 요소 주입기를 사용하여 템플릿에서 사용자 에이전트 영패를 성명적으로 동적으로 바꾸지 않습니다.반대로 우리는 컨디션을 강제로 바꿀 것이다.

관찰할 수 있는 상태를 창조하다


사용자 프록시 토큰을 대신하여 Internet Explorer 서비스는 별도의 브라우저 서비스에서 관찰할 수 있는 태그에 의존하게 됩니다.
// internet-explorer.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { BrowserService } from './browser.service';

@Injectable({
  providedIn: 'root',
})
export class InternetExplorerService {
  isInternetExplorer11$: Observable<boolean> =
    this.browser.userAgent$.pipe(
      map(userAgent => this.isInternetExplorer11(userAgent)),
    );

  constructor(
    private browser: BrowserService,
  ) {}

  isInternetExplorer11(userAgent: string): boolean {
    return /Trident\/7\.0.+rv:11\.0/.test(userAgent);
  }
}
// browser.service.ts

import { Inject, Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';

import { FakeUserAgent } from './fake-user-agent';
import { userAgentToken } from './user-agent.token';

@Injectable({
  providedIn: 'root',
})
export class BrowserService implements OnDestroy {
  private userAgent = new BehaviorSubject(this.realUserAgent);

  userAgent$ = this.userAgent.pipe(
    distinctUntilChanged(),
  );

  constructor(
    @Inject(userAgentToken) private realUserAgent: string,
  ) {}

  ngOnDestroy() {
    this.userAgent.complete();
  }

  fakeUserAgent(value: FakeUserAgent) {
    this.userAgent.next(FakeUserAgent[value]);
  }

  stopFakingUserAgent() {
    this.userAgent.next(this.realUserAgent);
  }
}
클래스 기반 서비스에서 볼 수 있는 브라우저 상태입니다.
현재 사용자 에이전트 상태는 BehaviorSubject<string>에 저장되며 이 상태는 userAgent$의 관찰가능BrowserService 속성에 공개됩니다.전체 응용 프로그램이 사용자 에이전트를 필요로 할 때, 이 관찰 가능한 대상에 의존해야 한다.
처음에 행위 주체는 사용자 에이전트 영패의 실제 사용자 에이전트 문자열에 의해 수합되었다.두 개의 명령을 통해 브라우저 상태를 변경할 수 있기 때문에, 이 값도 나중에 사용할 수 있도록 저장됩니다.
사용자 에이전트 상태를 가짜 사용자 에이전트 문자열로 설정하는 fakeUserAgent 방법을 공개했습니다.또한 dependee 호출 stopFakingUserAgent 방법을 허용합니다. 이 방법은 사용자 에이전트 상태를 실제 사용자 에이전트 문자열로 초기화합니다.
배의 정결을 유지하기 위해서, 우리는 심지어 서비스가 파괴되었을 때 행위 주제를 완성하는 것을 기억한다.
Internet Explorer 서비스는 현재 브라우저 서비스의 관찰가능 사용자 프록시 속성이 값을 낼 때마다 이 속성을 평가하는 isInternetExplorer11$라는 관찰가능 속성을 공개했다.
이제 deprecation banner 구성 요소를 관찰할 수 있는 인터넷 Explorer 11에 의존해서 속성을 검사할 수 있습니다. 우리가 바꾼 일반적인 속성이 아니라.
<!-- internet-explorer-11-banner.component.html -->
<aside *ngIf="isBannerVisible$ | async">
  Sorry, we will not continue to support Internet Explorer 11.<br />
  Please upgrade to Microsoft Edge.<br />

  <button (click)="onDismiss()">
    Dismiss
  </button>
</aside>
// internet-explorer-11-banner.component.ts
import { Component } from '@angular/core';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';

import { InternetExplorerService } from './internet-explorer.service';

@Component({
  host: { style: 'display: block;' },
  selector: 'internet-explorer-11-banner',
  templateUrl: './internet-explorer-11-banner.component.html',
})
export class InternetExplorer11BannerComponent {
  private isDismissed = new BehaviorSubject(false);

  isBannerVisible$ = combineLatest(
    this.internetExplorer.isInternetExplorer11$,
    this.isDismissed,
  ).pipe(
    map(([isInternetExplorer11, isDismissed]) =>
      isInternetExplorer11 && !isDismissed),
  );

  constructor(
    private internetExplorer: InternetExplorerService,
  ) {}

  onDismiss(): void {
    this.isDismissed.next(true);
  }
}
상태를 관찰할 수 있는 버려진 배너 구성 요소를 사용합니다.
deprecation banner 구성 요소에서 볼 isDismissed 속성을 하나 BehaviorSubject<boolean> 로 대체합니다. 이 속성은 최초로 삭제되었습니다. (false우리는 현재 관측 가능isBannerVisible$ 속성을 가지고 있는데 이것은 isDismissedInternetExplorerService#isInternetExplorer11$의 관측 가능한 상태의 조합이다.UI 비헤이비어 논리는 이전과 유사하지만 지금은 관찰 가능한 파이프의 일부로 표시됩니다.onDismiss 이벤트 처리 프로그램은 속성에 브리 값을 지정하는 것이 아니라 isDismissed 행위 테마를 통해 브리 값을 보냅니다.
이 때 응용 프로그램의 동작은 Internet Explorer 서비스 및 브라우저 서비스를 도입하기 전과 동일합니다.브라우저 상태 변경 명령이 있지만, 그것을 터치하기 위한 메커니즘이 필요합니다.
이를 위해, 우리는 응용 프로그램의 나머지 부분을 위해 브라우저 환경을 위조할 수 있는 브라우저 위조 구성 요소를 개발할 것이다.
<!-- browser-faker.component.html -->
<label>
  Fake a browser

  <select [formControl]="selectedBrowser">
    <option value="">
      My browser
    </option>

    <option *ngFor="let browser of browsers"
      [value]="browser">
      {{browser | replace:wordStartPattern:' $&' | trim}}
    </option>
  </select>
</label>
// browser-faker.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';

import { BrowserService } from './browser.service';
import { FakeUserAgent } from './fake-user-agent';

@Component({
  host: { style: 'display: block;' },
  selector: 'browser-faker',
  templateUrl: './browser-faker.component.html',
})
export class BrowserFakerComponent implements OnDestroy, OnInit {
  private defaultOptionValue = '';
  private destroy = new Subject<void>();
  private fakeBrowserSelection$: Observable<FakeUserAgent>;
  private realBrowserSelection$: Observable<void>;

  browsers = Object.keys(FakeUserAgent);
  selectedBrowser = new FormControl(this.defaultOptionValue);
  wordStartPattern = /[A-Z]|\d+/g;

  constructor(
    private browser: BrowserService,
  ) {
    this.realBrowserSelection$ = this.selectedBrowser.valueChanges.pipe(
      filter(value => value === this.defaultOptionValue),
      takeUntil(this.destroy),
    );
    this.fakeBrowserSelection$ = this.selectedBrowser.valueChanges.pipe(
      filter(value => value !== this.defaultOptionValue),
      takeUntil(this.destroy),
    );
  }

  ngOnInit(): void {
    this.bindEvents();
  }

  ngOnDestroy() {
    this.unbindEvents();
  }

  private bindEvents(): void {
    this.fakeBrowserSelection$.subscribe(userAgent =>
      this.browser.fakeUserAgent(userAgent));
    this.realBrowserSelection$.subscribe(() =>
      this.browser.stopFakingUserAgent());
  }

  private unbindEvents(): void {
    this.destroy.next();
    this.destroy.complete();
  }
}
브라우저가 구성 요소를 조작합니다.
브라우저가 구성 요소를 위조하여 브라우저 서비스에 주입합니다.이것은 본 컴퓨터 <select> 컨트롤에 연결된 폼 컨트롤이 있습니다.브라우저가 선택되었을 때, 우리는 브라우저 서비스를 통해 사용자 에이전트를 가장하기 시작했다.기본 브라우저 옵션이 선택되었을 때, 사용자 에이전트를 위장하는 것을 중지합니다.
테스트 중인 프로그램의 일부분으로 만들었습니다. a range of text pipes for component templates예를 들어 브라우저가 구성 요소를 위조하는 데 사용하는 replacetrim 파이프.
현재 우리는 브라우저 위조 구성 요소가 하나 있지만, 개발 과정에서 사용하기를 원할 뿐이다.개발 모델에서만 조건부로 나타나는 구조화 명령을 만듭니다.
// is-development-mode.token.ts
import { InjectionToken, isDevMode } from '@angular/core';

export const isDevelopmentModeToken: InjectionToken<boolean> =
  new InjectionToken('Development mode flag', {
    factory: (): boolean => isDevMode(),
    providedIn: 'root',
  });
// development-only.directive.ts
import {
  Directive,
  Inject,
  OnDestroy,
  OnInit,
  TemplateRef,
  ViewContainerRef,
} from '@angular/core';

import { isDevelopmentModeToken } from './is-development-mode.token';

@Directive({
  exportAs: 'developmentOnly',
  selector: '[developmentOnly]',
})
export class DevelopmentOnlyDirective implements OnDestroy, OnInit {
  private get isEnabled(): boolean {
    return this.isDevelopmentMode;
  }

  constructor(
    private container: ViewContainerRef,
    private template: TemplateRef<any>,
    @Inject(isDevelopmentModeToken) private isDevelopmentMode: boolean,
  ) {}

  ngOnInit(): void {
    if (this.isEnabled) {
      this.createAndAttachView();
    }
  }

  ngOnDestroy(): void {
    this.destroyView();
  }

  private createAndAttachView(): void {
    this.container.createEmbeddedView(this.template);
  }

  private destroyView(): void {
    this.container.clear();
  }
}
// development-only.directive.spec.ts
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { DevelopmentOnlyDirective } from './development-only.directive';
import { isDevelopmentModeToken } from './is-development-mode.token';

@Component({
  template: '<button *developmentOnly>God Mode</button>',
})
class TestComponent {}

describe(DevelopmentOnlyDirective.name, () => {
  function setup({ isDevelopmentMode }: { isDevelopmentMode: boolean }) {
    TestBed.configureTestingModule({
      declarations: [
        DevelopmentOnlyDirective,
        TestComponent,
      ],
      providers: [
        { provide: isDevelopmentModeToken, useValue: isDevelopmentMode },
      ],
    });

    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();

    const button = fixture.debugElement.query(By.css('button'));

    return {
      expectButtonToBeOmitted() {
        expect(button).toBe(null);
      },
      expectButtonToBeRendered() {
        expect(button.nativeElement).not.toBe(null);
        expect(button.nativeElement.textContent).toContain('God Mode');
      },
    };
  }

  it('renders its element in development mode', () => {
    const { expectButtonToBeRendered } = setup({ isDevelopmentMode: true });

    expectButtonToBeRendered();
  });

  it('omits its element in production mode', () => {
    const { expectButtonToBeOmitted } = setup({ isDevelopmentMode: false });

    expectButtonToBeOmitted();
  });
});
개발 구조 명령만 가능합니다.
만약 응용 프로그램이 개발 모드에서 실행되고 있다면, 이 구조화 명령은 연결된 구성 요소나 요소만 보여 줍니다. 이것은 응용 프로그램의 테스트 구성 요소가 검증한 것입니다.
이제 남은 것은 우리 프로그램에 버려진 플래카드와 가짜 브라우저를 추가하는 것이다.
<!-- app.component.html -->
<browser-faker *developmentOnly></browser-faker>
<internet-explorer-11-banner></internet-explorer-11-banner>

URL: <code><browser-url></browser-url></code>
// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent {}
Internet Explorer 11 기용 배너 및 브라우저 가짜가 포함된 각도 응용 프로그램.
이 프로그램은 a URL component도 포함하고 Location API각도 의존 관계로 보여 준다.
Internet Explorer 11이 위조된 경우 사용되지 않는 배너가 표시됩니다.
다른 브라우저가 위조되었을 때 플래카드를 사용하지 않는 것은 생략됩니다.
현재, 우리는 브라우저 환경을 위조하여 개발과 수동 테스트를 간소화할 수 있다.물론 진정한 Internet Explorer 11 브라우저에서 배너를 사용하지 않도록 테스트해야 합니다.참고 자료 부분에서 관련 도움을 찾으세요.

요약


사용자 환경을 모의할 수 있도록 브라우저 위조 구성 요소를 만들었습니다. 이 구성 요소는 개발 모델에서 조건부로 나타납니다.브라우저 상태를 클래스 기반 서비스에 봉하여 응용 프로그램에 의존하도록 합니다.이것은 브라우저 위조자가 사용하는 서비스와 같다.
브라우저 위조는 각도 프로그램에서 의존 관계를 위조하는 간단한 예이다.Angular 종속 주입 메커니즘을 동적으로 구성하는 다른 기술에 대해 논의했습니다.

리소스


각도 응용 프로그램에서 의존 관계를 위조하는 응용 프로그램이 a StackBlitz project 에 있는 것을 보여 줍니다.
각도 관련성을 테스트하고 위조하는 응용 프로그램에 사용되는 테스트 세트는 a separate StackBlitz project에 있습니다.
MicrosoftModern.IE 도메인 이름에는 무료 리소스가 있으며, Internet Explorer를 사용하여 브라우저 스냅샷을 생성할 수 있습니다.Windows 7 또는 8.1에서 Internet Explorer를 실행하는 무료 가상 머신 이미지도 제공합니다.

관련 기사


'' 의 각도 테스트 환경에서 의존 관계를 설정하고 해석하는 기술을 연구한다.
트리를 흔들 수 있는 의존항과 다른 복잡한 각도 의존항 주입 설정을 '' 에서 제공하는 방법을 이해합니다.이것은 우리의 응용 프로그램이 기반으로 한 문장이다.

논설 위원


Angular 커뮤니티의 우수 인사들이 이 글을 검토하는 데 도움을 주었습니다.


  • Brad Taniguchi






  • 좋은 웹페이지 즐겨찾기