亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

Angular應用中使用頁面對象模型設計模式進行測試

干净的代码:干净的测试

在构建软件应用时,我认为特别重要的有两方面:

  • 代码需要清晰、易于阅读和维护。要实现这一点,模块化非常重要。
  • 质量和自动化测试。我的代码需要经过充分的测试,我会努力自动化关键检查,以确保代码质量并快速自信地开发。

这篇文章讲的是如何把这两方面结合起来。说到底,编写测试代码其实还是在写代码——也得干净利落,要有模块化结构。

下面我们将测试Angular UI组件功能

UI 组件就像是现代 web 应用程序的积木。进行单元测试时,测试类和对应的 HTML 之间的集成非常重要,因为两者都对组件的功能很重要。

这叫做组件DOM测试,我认为这非常重要,值得专门写一篇文章来探讨。

Angular组件的DOM测试 在本文中,我将会展示Angular中DOM测试的好处和示例,虽然大多数概念……

本文将解释如何使用最常用的 Web 应用程序测试设计模式之一:页面对象模型 (Page Object Model),来编写干净的组件测试代码。

页面对象模型(Page Object Model,简称POM)设计模式

页面对象模型 (POM) 是一种用于 web 应用程序的自动化测试的设计模式,它有助于模块化和代码的重用。

它包括为应用程序的每个部分创建一个单独的类,该类中的方法与特定于该部分的HTML元素进行交互。

这个名字有点让人摸不着头脑

我故意用了“部分”这个词,而不是“页面”,因为我觉得“页面”这个词在这种情况下稍显误导。通常,“页面对象”是用来管理应用程序的特定部分,而不是整个页面。然而,尽管如此,“页面对象”这个词已经成为这种设计模式的标准术语并被广泛接受。

简而言之

页面对象模型通过封装所有与DOM交互的逻辑来抽象化UI的细节。

页面对象模式

这样,测试无需担心找到HTML元素或点击按钮:它们把这些任务交给页面对象来完成

分清“怎么做”和“做什么”

结果是,更清晰的代码和更好的职责分离。

  • 页面对象关注的是“如何” — 例如如何找到一个 HTML 元素,如何确定某个特定元素是否处于“夜间模式”。
  • 测试关注的是“什么” — 例如需要执行哪些操作以及期望的结果应该是什么。
在 Angular 中使用页面对象模型

当我提到这种设计模式时,有时有人会这么问我:“这不就是用来做e2e测试(e2e测试)的吗?”

是的,这种模式在端到端(e2e)测试中确实非常普遍。那么它是否也可以用于组件单元测试呢?当然可以!毕竟这是一种设计模式,每当你的测试需要与DOM交互时,它就会非常有用。甚至Angular文档也简要提到这一点。

我实际上在我的大多数Angular项目中都采用了这种模式。这就是我决定创建一个小型的Angular库的原因。这个库提供了一些基本工具,用于在Angular组件测试中使用页面对象,即ngx-page-object-model。它在_npm_上可用,轻量级、完全开源,并且可以与任何测试框架(如Jest、Jasmine、Vitest等)配合使用。它还可以与Spectator配合使用,或者单独使用。

我们来看一些代码示例,看看这个设计模式的实际应用!

这里有个代码例子
示例暗/亮模式切换组件

让我们来看一下名为ToggleStyleComponent的组件,它接受文本输入,并将其与一个按钮一起渲染,用于在亮模式和暗模式之间切换。

这里是组件的实现代码:

@Component({  
  selector: 'app-toggle-style',  
  standalone: true,  
  changeDetection: ChangeDetectionStrategy.OnPush,  
  template: `  
    <button  
      data-testid="toggle-dark-mode-button"  
      (click)="toggleDarkMode()"  
    > {{ toggleText() }} </button>  
    <div  
      data-testid="text-container"  
      [class]="isDarkModeEnabled() ? 'dark' : 'light'"  
    > {{ text() }} </div>  
  `,  
  styles: `  
    div {  
      padding: 20px;  
      margin-top: 10px;  
    }  
    .dark {  
      background-color: black;  
      color: white;  
    }  
    .light {  
      background-color: white;  
      color: black;  
    }  
  `,  
})  
export class ToggleStyleComponent {  
  readonly text = input<string>();  
  protected readonly isDarkModeEnabled = signal(false);  
  protected readonly toggleText = computed(() =>  
    this.isDarkModeEnabled()   
      ? '切换到浅模式'  
      : '切换到深模式',  
  );  
  protected toggleDarkMode(): void {  
    this.isDarkModeEnabled.set(!this.isDarkModeEnabled());  
  }  
}
测试计划方案

让我们为 ToggleStyleComponent 创建一个简单的测试计划。我们希望单元测试能检查以下几点:

  • 初始化时,组件应显示切换按钮和文本容器
  • 初始化时,组件应默认处于浅模式
  • 组件应渲染给定的 text
  • 当处于浅模式时,文本容器应具有 light CSS 类,而不具有 dark
  • 当处于浅模式时,按钮文本应为 “切换到深模式”
  • 当处于浅模式时,点击后应切换到深模式
  • 当处于深模式时,文本容器应具有 dark CSS 类,而不具有 light
  • 当处于深模式时,按钮文本应为“切换到浅模式”
  • 当处于深模式时,点击后应切换到浅模式
不使用 Page Object: 实现测试的示例

以下是我们不使用页面对象模型模式的情况下,为我们的 ToggleStyleComponent 实现上述测试计划的一个不太理想的方式的例子:

    import { ToggleStyleComponent } from './toggle-style.component';  
    import { TestBed } from '@angular/core/testing';  
    import { By } from '@angular/platform-browser';  

    describe(ToggleStyleComponent.name, () => {  
      beforeEach(async () => {  
        await TestBed.configureTestingModule({  
          imports: [ToggleStyleComponent],  
        }).compileComponents();  
      });  

      describe('初始化测试', () => {  
        it('应渲染一个切换按钮和一个文本区域', () => {  
          const fixture = TestBed.createComponent(ToggleStyleComponent);  

          fixture.detectChanges();  
          const textContainer = fixture.debugElement.query(By.css('div'));  
          const toggleButton = fixture.debugElement.query(By.css('button'));  

          expect(textContainer.nativeElement).toBeTruthy();  
          expect(toggleButton.nativeElement).toBeTruthy();  
        });  

        it('默认应为浅色模式', () => {  
          const fixture = TestBed.createComponent(ToggleStyleComponent);  

          fixture.detectChanges();  
          const textContainer = fixture.debugElement.query(By.css('div'));  

          expect(textContainer.nativeElement.classList).toContain('light');  
          expect(textContainer.nativeElement.classList).not.toContain('dark');  
        });  

        it('应显示指定的文本', () => {  
          const fixture = TestBed.createComponent(ToggleStyleComponent);  

          fixture.componentRef.setInput('text', 'My text');  
          fixture.detectChanges();  
          const textContainer = fixture.debugElement.query(By.css('div'));  

          expect(textContainer.nativeElement.textContent).toContain('My text');  
        });  
      });  

      describe('浅色主题', () => {  
        it('应正确显示样式和按钮文字', () => {  
          const fixture = TestBed.createComponent(ToggleStyleComponent);  

          fixture.detectChanges();  
          const textContainer = fixture.debugElement.query(By.css('div'));  
          const toggleButton = fixture.debugElement.query(By.css('button'));  

          expect(textContainer.nativeElement.classList).toContain('light');  
          expect(textContainer.nativeElement.classList).not.toContain('dark');  
          expect(toggleButton.nativeElement.textContent).toContain(  
            '切换到深色模式',  
          );  
        });  

        it('点击后应切换至深色模式', () => {  
          const fixture = TestBed.createComponent(ToggleStyleComponent);  

          fixture.detectChanges();  
          const textContainer = fixture.debugElement.query(By.css('div'));  
          const toggleButton = fixture.debugElement.query(By.css('button'));  

          toggleButton.nativeElement.click();  
          fixture.detectChanges();  

          expect(textContainer.nativeElement.classList).toContain('dark');  
          expect(textContainer.nativeElement.classList).not.toContain('light');  
        });  
      });  

      describe('深色主题', () => {  
        it('应正确显示样式和按钮文字', () => {  
          const fixture = TestBed.createComponent(ToggleStyleComponent);  

          fixture.detectChanges();  
          const textContainer = fixture.debugElement.query(By.css('div'));  
          const toggleButton = fixture.debugElement.query(By.css('button'));  
          toggleButton.nativeElement.click();  
          fixture.detectChanges();  

          expect(textContainer.nativeElement.classList).toContain('dark');  
          expect(textContainer.nativeElement.classList).not.toContain('light');  
          expect(toggleButton.nativeElement.textContent).toContain(  
            '切换到浅色模式',  
          );  
        });  

        it('点击后应切换至浅色模式', () => {  
          const fixture = TestBed.createComponent(ToggleStyleComponent);  

          fixture.detectChanges();  
          const textContainer = fixture.debugElement.query(By.css('div'));  
          const toggleButton = fixture.debugElement.query(By.css('button'));  
          toggleButton.nativeElement.click();  
          fixture.detectChanges();  

          toggleButton.nativeElement.click();  
          fixture.detectChanges();  

          expect(textContainer.nativeElement.classList).toContain('light');  
          expect(textContainer.nativeElement.classList).not.toContain('dark');  
        });  
      });  
    });

你已经能感觉到我们的测试肯定还有改进的空间,你觉得呢?

页面对象测试实现示例

我们先创建一个页面对象,它将负责:

  • 定位 HTML 元素
  • 与它们交互
  • 判断是亮模式还是暗模式

要做到这一点,我们只需创建一个新的 Page 类,该类继承了 PageObjectModel 基类,该基类由 ngx-page-object-model 库提供,并实现我们自己的方法。

要获取HTML元素,我们仍然可以用标准的CSS选择器,例如divbutton,但使用data-testid属性被认为更好(更多详情请参阅这里)。

代码相当简单,一看就懂:

    import { DebugHtmlElement, PageObjectModel } from 'ngx-page-object-model';  

    class Page extends PageObjectModel<ToggleStyleComponent> {  
      // 定义元素访问方法  
      textContainer(): DebugHtmlElement<HTMLDivElement> {  
        return this.getDebugElementByTestId('text-container');  
      }  
      toggleButton(): DebugHtmlElement<HTMLButtonElement> {  
        return this.getDebugElementByTestId('toggle-dark-mode-button');  
      }  

      // 定义操作方法  
      getRenderedText(): string | null {  
        return this.textContainer().nativeElement.textContent;  
      }  
      clickToggleMode(): void {  
        this.toggleButton().nativeElement.click();  
        detectChanges();  
      }  

      // 定义可重用的期望宏  
      expectLightModeActive(): void {  
        const containerClass = this.textContainer().nativeElement.classList;  
        expect(containerClass).toContain('light');  
        expect(containerClass).not.toContain('dark');  
        expect(toggleButton().nativeElement.textContent).toContain(  
          '切换到夜间模式',  
        );  
      }  
      expectDarkModeActive(): void {  
        const containerClass = this.textContainer().nativeElement.classList;  
        expect(containerClass).toContain('dark');  
        expect(containerClass).not.toContain('light');  
        expect(toggleButton().nativeElement.textContent).toContain(  
          '切换到日间模式',  
        );  
      }  
    }

现在,在我们的测试里,我们可以通过将组件的 fixture 传递给构造器来创建 Page 类型的一个实例。

const page = new Page(TestBed.createComponent(ToggleStyleComponent)); // 创建一个新的Page对象,参数为通过TestBed创建的ToggleStyleComponent组件实例

我们使用 page 对象来进行测试,看起来像这样:

    it('应渲染一个切换按钮 <button> 和一个文本容器 <div>', () => {  
      定义页面为 new Page(TestBed.createComponent(ToggleStyleComponent));  
      page.detectChanges();  

      expect(page.textContainer()).toBeTruthy();  
      expect(page.toggleButton()).toBeTruthy();  
    });  

    it('默认应处于浅色模式', () => {  
      定义页面为 new Page(TestBed.createComponent(ToggleStyleComponent));  
      page.detectChanges();  

      // 现在会全面检查所有浅色模式属性  
      page.expectLightModeActive();  
    });  

    it('应显示指定的文本', () => {  
      定义页面为 new Page(TestBed.createComponent(ToggleStyleComponent));  
      page.fixture.componentRef.setInput('text', 'My text');  
      page.detectChanges();  

      expect(page.getRenderedText()).toContain('My text');  
    });  

    it('点击按钮后应切换到深色模式', () => {  
      定义页面为 new Page(TestBed.createComponent(ToggleStyleComponent));  
      page.detectChanges();  

      page.clickToggleMode();  

      // 现在会全面检查所有深色模式属性  
      page.expectDarkModeActive();  
    });  

    it('再次点击按钮后应切换回浅色模式', () => {  
      定义页面为 new Page(TestBed.createComponent(ToggleStyleComponent));  
      page.detectChanges();  

      page.clickToggleMode();  
      page.clickToggleMode();  

      page.expectLightModeActive();  
    });

所以:

  • 测试代码看起来更整洁、更易读。
  • 不再有重复的代码。
  • 由于 page.expectLightModeActive()page.expectDarkModeActive() 已经充分检查了每种模式的正确性,所以需要的测试用例减少了。
  • 测试代码可以完全专注于需要做什么和检查什么,而无需担心实现细节——页面对象内部抽象了这些实现细节。

您可以在此处获取此示例的完整源代码。

Angular UI组件测试更多内容

检查 ngx-page-object-model 文档,在那里你可以找到更多关于此主题的功能、示例、最佳实践和技术等内容。

我们来总结一下
  • 无论是自动化测试还是代码模块化,都是构建可扩展应用程序的关键组成部分。
  • 测试代码质量同样重要,它应该具有模块化和可维护性。
  • 对于用户界面组件的单元测试,应该模仿用户与其交互的方式。
  • 页面对象模型(Page Object Model,简称POM)是一种设计模式,它通过创建一个抽象层来帮助编写模块化的测试代码,使得测试代码更易于管理和维护,特别是在与DOM交互时。
  • ngx-page-object-model库可以帮助轻松地将该设计模式集成到Angular组件的测试中。
點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消