返回博客首页
← 所有文章

ns test init - @nativescript/unit-test-runner v3 版本发布详情 - 测试您的应用、插件以及平台 API 💪 + SonarCloud 等集成额外功能

2021 年 12 月 14 日 — 作者:技术指导委员会 (TSC)

通过 @nativescript/unit-test-runner v3,我们提高了它的多功能性和易用性。我们还添加了一个简单的 CLI 标记来启用代码覆盖率报告 (--env.codeCoverage)。在查看如何设置和运行它之前,让我们先看一个简单的测试示例。

import { isIOS } from '@nativescript/core';

class AnyClass {
  hello = `hello ${isIOS ? 'from ios' : 'from android'}`;

  buttonTap() {
    const message = 'hello from platform native apis';
    if (isIOS) {
      console.log(NSString.stringWithString(message + ' on ios').toString());
    } else {
      console.log(new java.lang.String(message + ' on android').toString());
    }
  }
}

describe('Test AnyClass including platform native APIs', () => {
  let anything: AnyClass;

  beforeEach(() => {
    anything = new AnyClass();
    spyOn(console, 'log');
  });

  it("sanity check", () => {
    expect(anything).toBeTruthy();
    expect(anything.hello).toBe(`hello ${isIOS ? 'from ios' : 'from android'}`);
  });

  it('buttonTap should invoke platform native apis', () => {
    anything.buttonTap();
    expect(console.log).toHaveBeenCalledWith(
      `hello from platform native apis on ${isIOS ? 'ios' : 'android'}`
    );
  });
});

您可以流畅地测试 iOS 或 Android 上的任何平台原生 API,并确认您的应用逻辑是否合理以及是否符合您的预期。

unit-test-results

设置

从 NativeScript CLI 8.1.5 版本开始(*可以使用 npm i -g nativescript 随时安装最新 CLI*),v3 单元测试运行器将在运行以下命令时自动设置

ns test init

然后,您可以为任何目标平台运行单元测试。

ns test ios
// or:
ns test android

它们默认以观察模式运行,并且运行非常高效,并持续进行实时更新。

生成代码覆盖率报告

ns test ios --env.codeCoverage

ns test android --env.codeCoverage

然后,您可以打开 coverage/index.html 文件以查看覆盖率报告。

unit-test-results

关于覆盖率的说明

默认情况下,报告仅引用测试涉及的文件,覆盖率百分比反映该代码集。

如果您希望在报告中包含所有代码,包括那些根本没有被测试覆盖的文件,您可以使用插件 karma-sabarivka-reporter

  • 安装插件

    npm install --save-dev karma-sabarivka-reporter 
    
    
  • 更新 karma.conf.js

    将 sabarivka 添加到报告程序数组中

    reporters: [
    // ...
    'sabarivka'
    // ...
    ],
    

    向 coverageReporter 配置添加 include 属性

    coverageReporter: {
      // ...
        include: [
          // Specify include pattern(s) first
          'src/**/*.(ts|js)',
          // Then specify "do not touch" patterns 
          // (note `!` sign on the beginning of each statement)
          '!src/**/*.spec.(ts|js)',
      //...
      ]
    },
    

下次启用覆盖率运行测试时,覆盖率百分比应反映整个代码库,并且报告应包含没有测试的文件。

测试插件

如果您通过 Nxyarn workspaces 或任何其他项目设置(在其中您在应用代码库旁边有内部管理的插件)管理自己的插件、插件套件或应用,您现在也可以轻松地收集这些测试(*甚至将它们包含在您的覆盖率报告中*)。

修改 test.ts 入口文件以包含来自**应用外部**的源代码。

import { runTestApp } from '@nativescript/unit-test-runner';
declare let require: any;

runTestApp({
  runTests: () => {
    // tests inside your app
    const tests = require.context("./", true, /\.spec\.ts$/);
    tests.keys().map(tests);

    // tests outside of your app, like internally managed plugins in a workspace style setup
    const pluginTests = require.context('../plugins/my-internal-plugin', true, /\.spec\.ts$/);
    pluginTests.keys().map(pluginTests);
  },
});

您可以探索此处演示此功能的示例仓库

示例:Angular

您可以在所有版本中获得改进的测试运行器的优势,但让我们以 Angular 为例,并重点介绍它在实践中的用法,使用相同的示例。

此外,我们将添加一个巧妙的 dumpView 实用程序,它将视图结构打印为字符串,我们可以用它来测试给定绑定时视图渲染是否正常工作。您可以创建任意数量对您和您的团队的测试方法有用的实用程序。例如,您可以创建一个对象来遍历视图节点进行测试,而不是创建视图绑定的字符串表示形式。

import { Component } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';
import { isIOS } from '@nativescript/core';
import { dumpView } from '../unit-test-utils';

@Component({
  template: '<StackLayout><Label [text]="hello"></Label></StackLayout>',
})
class AnyComponent {
  hello = `hello ${isIOS ? 'from ios' : 'from android'}`;

  buttonTap() {
    const message = 'hello from native apis';
    if (isIOS) {
      console.log(NSString.stringWithString(message + ' on ios').toString());
    } else {
      console.log(new java.lang.String(message + ' on android').toString());
    }
  }
}

describe('AnyComponent', () => {
  let component: AnyComponent;
  let fixture: ComponentFixture<AnyComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [AnyComponent],
    }).compileComponents();
    fixture = TestBed.createComponent(AnyComponent);
    fixture.detectChanges();
    component = fixture.componentInstance;
    spyOn(console, 'log');
  });

  it('sanity check', () => {
    expect(component).toBeTruthy();
    expect(component.hello).toBe(
      `hello ${isIOS ? 'from ios' : 'from android'}`
    );
  });

  it('view binding handles iOS/Android specific behavior', () => {
    expect(dumpView(fixture.nativeElement, true)).toBe(
      `(proxyviewcontainer (stacklayout (label[text=hello ${
        isIOS ? 'from ios' : 'from android'
      }])))`
    );
  });

  it('buttonTap should invoke native apis', () => {
    component.buttonTap();
    expect(console.log).toHaveBeenCalledWith([
      `hello from native apis on ${isIOS ? 'ios' : 'android'}`,
    ]);
  });
});
  • unit-test-utils.ts
export function dumpView(view: View, verbose: boolean = false): string {
  let nodeName: string = (<any>view).nodeName;
  if (!nodeName) {
    // Strip off the source
    nodeName = view.toString().replace(/(@[^;]*;)/g, '');
  }
  nodeName = nodeName.toLocaleLowerCase();

  let output = ['(', nodeName];
  if (verbose) {
    if (view instanceof TextBase) {
      output.push('[text=', view.text, ']');
    }
  }

  let children = getChildren(view)
    .map((c) => dumpView(c, verbose))
    .join(', ');
  if (children) {
    output.push(' ', children);
  }

  output.push(')');
  return output.join('');
}

function getChildren(view: View): Array<View> {
  let children: Array<View> = [];
  (<any>view).eachChildView((child: View) => {
    children.push(child);
    return true;
  });
  return children;
}

要使用 Angular 进行单元测试,您还需要确保您的主要测试入口也配置了 Angular 测试环境。

  • test.ts
import { runTestApp } from '@nativescript/unit-test-runner';
declare let require: any;

runTestApp({
  runTests: () => {
    const tests = require.context('./', true, /\.spec\.ts$/);
    // ensure main.spec is included first
    // to configure Angular's test environment
    tests('./main.spec.ts'); 
    tests.keys().map(tests);
  },
});
  • main.spec.ts
import './polyfills';
import 'zone.js/dist/zone-testing.js';
import { TestBed } from '@angular/core/testing';
import { platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
import { NativeScriptTestingModule } from '@nativescript/angular/testing';

TestBed.initTestEnvironment(
  NativeScriptTestingModule,
  platformBrowserDynamicTesting(),
  { teardown: { destroyAfterEach: true } }
);

集成额外功能 - SonarSource - SonarCloud

您可以使用 v3 运行器连接许多不错的集成。

例如,我们将与 SonarCloud 集成,它是一种基于云的代码质量和安全服务。

他们为公共开源项目提供免费账户,因此您可以开通一个免费账户来试用一下。

修改 coverageReporter

修改 coverageReporter 以包含 SonarCloud 预期的报告程序类型。

  • karma.conf.js
coverageReporter: {
    dir: require('path').join(__dirname, './coverage'),
    subdir: '.',
    reporters: [
        { type: 'lcovonly' },
        { type: 'text-summary' }
    ]
},

包含单元测试报告

npm i karma-sonarqube-unit-reporter --save-dev

现在修改 reporters 以包含 sonarqubeUnit 并为其添加配置。

  • karma.conf.js
// add the reporter
reporters: ['progress', 'sonarqubeUnit'], 

// add the configuration
sonarQubeUnitReporter: {
    sonarQubeVersion: 'LATEST',
    outputDir: require('path').join(__dirname, './SonarResults'),
    outputFile: 'ut_report.xml',
    useBrowserName: false,
    overrideTestDescription: true,
},

发布到 Sonar

现在,当您执行测试时,将生成两个报告。

  1. 覆盖率:./coverage/lcov.info
  2. 单元测试报告:./SonarResults/ut_report.xml

Sonarcloud 提供了一个与您的平台匹配的脚本(例如 ./sonarscan.sh),该脚本执行分析和报告发布,他们在您设置项目时提供指导和下载链接。

对于 NativeScript,您必须将一些属性传递给 sonar 以让它知道您的配置。

  1. sonar.typescript.tsconfigPath

    Sonar 需要一个简单的 tsconfig 文件来查找所有 ts 文件。在项目的根目录创建一个单独的文件 (tsconfig.sonar.json)。

     {
       "extends": "./tsconfig.json",
       "include": [
         "./src/**/*.ts",
         "**/*.d.ts"
       ]
     }
    
  2. sonar.tests

    包含测试文件的目录。

  3. sonar.test.inclusions

    与测试文件匹配的文件模式。

  4. sonar.testExecutionReportPath

    测试执行报告的文件模式。

  5. sonar.javascript.lcov.reportPaths

    覆盖率报告的文件模式。

要传递的示例属性

      -Dsonar.typescript.tsconfigPath=./tsconfig.sonar.json
      -Dsonar.tests=./src/tests  
      -Dsonar.test.inclusions=**/*.spec.ts   
      -Dsonar.sources=./src
      -Dsonar.testExecutionReportPaths=SonarResults\ut_report.xml
      -Dsonar.javascript.lcov.reportPaths=coverage\**\*.info

运行扫描程序,您的配置文件现在包含单元测试和覆盖率报告。

unit-test-sonarcloud

测试能否保证 0 bug?

每个人都希望能够声称这一点,但测试只能帮助减少 bug 以及提高团队对代码在团队测试条件下运行方式的信心。随着代码的发展,它们还可以帮助防止回归,因为它们为您期望以某种方式工作的区域提供了覆盖率,并且可以非常快速地告诉您,如果由于将来出现的更改而导致您期望成功的事情突然失败。

TypeScript 本身是否有助于防止 bug?

它绝对有帮助 - 在整个代码库中增加强类型检查可以帮助增强代码的完整性,这可以提高代码的持久性和适应未来变化的容易程度。但是,TypeScript 本身并不能取代适当的单元测试,因为 TypeScript 关注的是代码完整性,而单元测试则关注的是代码运行操作性和行为的逻辑成功/失败。结合使用 Eslint 和 TypeScript 可以进一步指导最佳实践的实施。