Compartilhar via


Tutorial: Adicionar testes de unidade a projetos de visual do Power BI

Este artigo descreve as noções básicas de como escrever testes de unidade para seus visuais do Power BI, incluindo como:

  • Configurar a estrutura de teste do executor de teste do Karma JavaScript, Jasmine.
  • Usar o pacote powerbi-visuals-utils-testutils.
  • Usar simulações e elementos fictícios para ajudar a simplificar o teste de unidade de visuais do Power BI.

Pré-requisitos

  • Um projeto de visuais do Power BI instalado
  • Um ambiente do Node.js configurado

Os exemplos neste artigo usam o visual de gráfico de barras para teste.

Instalar e configurar o executor de teste do Karma JavaScript e o Jasmine

Adicione as bibliotecas necessárias ao arquivo package.json na seção devDependencies:

"@types/d3": "5.7.2",
"@types/d3-selection": "^1.0.0",
"@types/jasmine": "^3.10.2",
"@types/jasmine-jquery": "^1.5.34",
"@types/jquery": "^3.5.8",
"@types/karma": "^6.3.1",
"@types/lodash-es": "^4.17.5",
"coveralls": "^3.1.1",
"d3": "5.12.0",
"jasmine": "^3.10.0",
"jasmine-core": "^3.10.1",
"jasmine-jquery": "^2.1.1",
"jquery": "^3.6.0",
"karma": "^6.3.9",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage": "^2.0.3",
"karma-coverage-istanbul-reporter": "^3.0.3",
"karma-jasmine": "^4.0.1",
"karma-junit-reporter": "^2.0.1",
"karma-sourcemap-loader": "^0.3.8",
"karma-typescript": "^5.5.2",
"karma-typescript-preprocessor": "^0.4.0",
"karma-webpack": "^5.0.0",
"powerbi-visuals-api": "^3.8.4",
"powerbi-visuals-tools": "^3.3.2",
"powerbi-visuals-utils-dataviewutils": "^2.4.1",
"powerbi-visuals-utils-formattingutils": "^4.7.1",
"powerbi-visuals-utils-interactivityutils": "^5.7.1",
"powerbi-visuals-utils-tooltiputils": "^2.5.2",
"puppeteer": "^11.0.0",
"style-loader": "^3.3.1",
"ts-loader": "~8.2.0",
"ts-node": "^10.4.0",
"tslint": "^5.20.1",
"tslint-microsoft-contrib": "^6.2.0"

Para saber mais sobre package.json, consulte a descrição em npm-package.json.

Salve o arquivo package.json e execute o seguinte comando no local do arquivo package.json:

npm install

O gerenciador de pacotes instala todos os novos pacotes que são adicionados ao package.json.

Para executar testes de unidade, configure o executor e a configuração webpack.

O código a seguir é um exemplo do arquivo test.webpack.config.js:

const path = require('path');
const webpack = require("webpack");

module.exports = {
    devtool: 'source-map',
    mode: 'development',
    optimization : {
        concatenateModules: false,
        minimize: false
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            },
            {
                test: /\.json$/,
                loader: 'json-loader'
            },
            {
                test: /\.tsx?$/i,
                enforce: 'post',
                include: /(src)/,
                exclude: /(node_modules|resources\/js\/vendor)/,
                loader: 'istanbul-instrumenter-loader',
                options: { esModules: true }
            },
            {
                test: /\.less$/,
                use: [
                    {
                        loader: 'style-loader'
                    },
                    {
                        loader: 'css-loader'
                    },
                    {
                        loader: 'less-loader',
                        options: {
                            paths: [path.resolve(__dirname, 'node_modules')]
                        }
                    }
                ]
            }
        ]
    },
    externals: {
        "powerbi-visuals-api": '{}'
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js', '.css']
    },
    output: {
        path: path.resolve(__dirname, ".tmp/test")
    },
    plugins: [
        new webpack.ProvidePlugin({
            'powerbi-visuals-api': null
        })
    ]
};

O código a seguir é um exemplo do arquivo karma.conf.ts:

"use strict";

const webpackConfig = require("./test.webpack.config.js");
const tsconfig = require("./test.tsconfig.json");
const path = require("path");

const testRecursivePath = "test/visualTest.ts";
const srcOriginalRecursivePath = "src/**/*.ts";
const coverageFolder = "coverage";

process.env.CHROME_BIN = require("puppeteer").executablePath();

import { Config, ConfigOptions } from "karma";

module.exports = (config: Config) => {
    config.set(<ConfigOptions>{
        mode: "development",
        browserNoActivityTimeout: 100000,
        browsers: ["ChromeHeadless"], // or specify Chrome to use the locally installed Chrome browser
        colors: true,
        frameworks: ["jasmine"],
        reporters: [
            "progress",
            "junit",
            "coverage-istanbul"
        ],
        junitReporter: {
            outputDir: path.join(__dirname, coverageFolder),
            outputFile: "TESTS-report.xml",
            useBrowserName: false
        },
        singleRun: true,
        plugins: [
            "karma-coverage",
            "karma-typescript",
            "karma-webpack",
            "karma-jasmine",
            "karma-sourcemap-loader",
            "karma-chrome-launcher",
            "karma-junit-reporter",
            "karma-coverage-istanbul-reporter"
        ],
        files: [
            "node_modules/jquery/dist/jquery.min.js",
            "node_modules/jasmine-jquery/lib/jasmine-jquery.js",
            {
                pattern: './capabilities.json',
                watched: false,
                served: true,
                included: false
            },
            testRecursivePath,
            {
                pattern: srcOriginalRecursivePath,
                included: false,
                served: true
            }
        ],
        preprocessors: {
            [testRecursivePath]: ["webpack", "coverage"]
        },
        typescriptPreprocessor: {
            options: tsconfig.compilerOptions
        },
        coverageIstanbulReporter: {
            reports: ["html", "lcovonly", "text-summary", "cobertura"],
            dir: path.join(__dirname, coverageFolder),
            'report-config': {
                html: {
                    subdir: 'html-report'
                }
            },
            combineBrowserReports: true,
            fixWebpackSourcePaths: true,
            verbose: false
        },
        coverageReporter: {
            dir: path.join(__dirname, coverageFolder),
            reporters: [
                // reporters not supporting the `file` property
                { type: 'html', subdir: 'html-report' },
                { type: 'lcov', subdir: 'lcov' },
                // reporters supporting the `file` property, use `subdir` to directly
                // output them in the `dir` directory
                { type: 'cobertura', subdir: '.', file: 'cobertura-coverage.xml' },
                { type: 'lcovonly', subdir: '.', file: 'report-lcovonly.txt' },
                { type: 'text-summary', subdir: '.', file: 'text-summary.txt' },
            ]
        },
        mime: {
            "text/x-typescript": ["ts", "tsx"]
        },
        webpack: webpackConfig,
        webpackMiddleware: {
            stats: "errors-only"
        }
    });
};

Se necessário, você pode modificar essa configuração.

O código em karma.conf.js contém as seguintes variáveis:

  • recursivePathToTests: localiza o código de teste.

  • srcRecursivePath: localiza o código JavaScript de saída após a compilação.

  • srcCssRecursivePath: localiza o CSS de saída depois de compilar menos o arquivo com estilos.

  • srcOriginalRecursivePath: localiza o código-fonte do visual.

  • coverageFolder: determina o local em que o relatório de cobertura deve ser criado.

O arquivo de configuração inclui as seguintes propriedades:

  • singleRun: true: Os testes são executados em um sistema de CI (integração contínua) ou podem ser executados uma vez. Você pode alterar a configuração para false para depurar seus testes. A estrutura Karma mantém o navegador em execução para que você possa usar o console para depuração.

  • files: [...]: Nessa matriz, você pode especificar os arquivos a serem carregados no navegador. Os arquivos carregados normalmente são arquivos de origem, casos de teste e bibliotecas (como Jasmine ou utilitários de teste). Você pode adicionar mais arquivos conforme necessário.

  • preprocessors: Nesta seção, você configurará ações executadas antes da execução dos testes de unidade. As ações podem pré-compilar o TypeScript para JavaScript, preparar os arquivos de source map e gerar um relatório de cobertura de código. Você pode desabilitar coverage ao depurar seus testes. coverage gera mais código para testes de cobertura de código, o que complica os testes de depuração.

Para obter descrições de todas as configurações do karma, vá para a página Arquivo de Configuração do Karma.

Para sua conveniência, você pode adicionar um comando de teste em scripts em package.json:

{
    "scripts": {
        "pbiviz": "pbiviz",
        "start": "pbiviz start",
        "typings":"node node_modules/typings/dist/bin.js i",
        "lint": "tslint -r \"node_modules/tslint-microsoft-contrib\"  \"+(src|test)/**/*.ts\"",
        "pretest": "pbiviz package --resources --no-minify --no-pbiviz --no-plugin",
        "test": "karma start"
    }
    ...
}

Agora você está pronto para começar a escrever seus testes de unidade.

Verificar o elemento DOM do visual

Para testar o Visual, primeiro crie uma instância do visual.

Criar um construtor de instância do visual

Adicione um arquivo visualBuilder.ts à pasta de test usando o seguinte código:

import { VisualBuilderBase } from "powerbi-visuals-utils-testutils";

import { BarChart as VisualClass } from "../src/barChart";

import powerbi from "powerbi-visuals-api";
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;

export class BarChartBuilder extends VisualBuilderBase<VisualClass> {
  constructor(width: number, height: number) {
    super(width, height);
  }

  protected build(options: VisualConstructorOptions) {
    return new VisualClass(options);
  }

  public get mainElement() {
    return $(this.element).children("svg.barChart");
  }
}

O método build cria uma instância do visual. mainElement é um método Get, que retorna uma instância do elemento DOM (Modelo de Objeto do Documento) de uma raiz em seu visual. O getter é opcional, mas facilita a gravação do teste de unidade.

Agora você tem um build de uma instância do seu visual. Vamos escrever o caso de teste. O exemplo de caso de teste verifica os elementos SVG criados quando o visual é exibido.

Criar um arquivo TypeScript para escrever casos de teste

Adicione um arquivo visualTest.ts para os casos de teste usando o seguinte código:

import powerbi from "powerbi-visuals-api";

import { BarChartBuilder } from "./visualBuilder";
import { SampleBarChartDataBuilder } from "./visualData";

import DataView = powerbi.DataView;

describe("BarChart", () => {
  let visualBuilder: BarChartBuilder;
  let dataView: DataView;
  let defaultDataViewBuilder: SampleBarChartDataBuilder;

  beforeEach(() => {
    visualBuilder = new BarChartBuilder(500, 500);
    defaultDataViewBuilder = new SampleBarChartDataBuilder();
    dataView = defaultDataViewBuilder.getDataView();
  });

  it("root DOM element is created", () => {
    visualBuilder.updateRenderTimeout(dataView, () => {
      expect(visualBuilder.mainElement[0]).toBeInDOM();
    });
  });
});

Vários métodos Jasmine são chamados:

  • describe: Descreve um caso de teste. No contexto da estrutura do Jasmine, describe geralmente descreve um conjunto ou grupo de especificações.

  • beforeEach: É chamado antes de cada chamada do método it, que é definida no método describe.

  • it: Define uma única especificação. O método it deve conter um ou mais expectations.

  • expect: Cria uma expectativa para uma especificação. Uma especificação tem êxito se todas as expectativas são aprovadas sem falhas.

  • toBeInDOM: é um s métodos de correspondências. Para obter mais informações sobre correspondências, confira Namespace do Jasmine: correspondências.

Para obter mais informações sobre o Jasmine, confira a página Documentação da estrutura do Jasmine.

Iniciar testes de unidade

Esse teste verifica se o elemento SVG raiz do visual existe quando o visual é executado. Para executar o teste de unidade, digite o seguinte comando na ferramenta de linha de comando:

npm run test

karma.js executa o caso de teste no navegador Chrome.

Screenshot of the Chrome browser, which shows that karma dot js is running the test case.

Observação

Você deve instalar o Google Chrome localmente.

Na janela de linha de comando, você obterá a seguinte saída:

> karma start

23 05 2017 12:24:26.842:WARN [watcher]: Pattern "E:/WORKSPACE/PowerBI/PowerBI-visuals-sampleBarChart/data/*.csv" does not match any file.
23 05 2017 12:24:30.836:WARN [karma]: No captured browser, open https://localhost:9876/
23 05 2017 12:24:30.849:INFO [karma]: Karma v1.3.0 server started at https://localhost:9876/
23 05 2017 12:24:30.850:INFO [launcher]: Launching browser Chrome with unlimited concurrency
23 05 2017 12:24:31.059:INFO [launcher]: Starting browser Chrome
23 05 2017 12:24:33.160:INFO [Chrome 58.0.3029 (Windows 10 0.0.0)]: Connected on socket /#2meR6hjXFmsE_fjiAAAA with id 5875251
Chrome 58.0.3029 (Windows 10 0.0.0): Executed 1 of 1 SUCCESS (0.194 secs / 0.011 secs)

=============================== Coverage summary ===============================
Statements   : 27.43% ( 65/237 )
Branches     : 19.84% ( 25/126 )
Functions    : 43.86% ( 25/57 )
Lines        : 20.85% ( 44/211 )
================================================================================

Como adicionar dados estáticos para testes de unidade

Crie o arquivo visualData.ts na pasta test usando o seguinte código:

import powerbi from "powerbi-visuals-api";
import DataView = powerbi.DataView;

import { testDataViewBuilder } from "powerbi-visuals-utils-testutils";

import TestDataViewBuilder = testDataViewBuilder.TestDataViewBuilder;

export class SampleBarChartDataBuilder extends TestDataViewBuilder {
  public static CategoryColumn: string = "category";
  public static MeasureColumn: string = "measure";

  public getDataView(columnNames?: string[]): DataView {
    let dateView: any = this.createCategoricalDataViewBuilder(
      [
          ...
      ],
      [
          ...
      ],
      columnNames
    ).build();

    // there's client side computed maxValue
    let maxLocal = 0;
    this.valuesMeasure.forEach((item) => {
      if (item > maxLocal) {
        maxLocal = item;
      }
    });
    (<any>dataView).categorical.values[0].maxLocal = maxLocal;

    return dataView;
  }
}

A classe SampleBarChartDataBuilder estende TestDataViewBuilder e implementa o método abstrato getDataView.

Quando você coloca dados em buckets de campos de dados, o Power BI produz um objeto categórico dataview baseado em seus dados.

Screenshot of Power BI, which shows the data fields buckets are empty.

Em testes de unidade, você não tem acesso a funções principais do Power BI que normalmente usa para reproduzir os dados. Mas você precisa mapear seus dados estáticos para dataview categóricos. Use a classe TestDataViewBuilder para mapear seus dados estáticos.

Para obter mais informações sobre mapeamento de Exibição de Dados, confira DataViewMappings.

No método getDataView, você chama o método createCategoricalDataViewBuilder com seus dados.

No arquivo capabilities.json do visual sampleBarChart, temos os objetos dataRoles e dataViewMapping:

"dataRoles": [
    {
        "displayName": "Category Data",
        "name": "category",
        "kind": "Grouping"
    },
    {
        "displayName": "Measure Data",
        "name": "measure",
        "kind": "Measure"
    }
],
"dataViewMappings": [
    {
        "conditions": [
            {
                "category": {
                    "max": 1
                },
                "measure": {
                    "max": 1
                }
            }
        ],
        "categorical": {
            "categories": {
                "for": {
                    "in": "category"
                }
            },
            "values": {
                "select": [
                    {
                        "bind": {
                            "to": "measure"
                        }
                    }
                ]
            }
        }
    }
],

Para gerar o mesmo mapeamento, você precisa definir os seguintes parâmetros para o método createCategoricalDataViewBuilder:

([
    {
        source: {
            displayName: "Category",
            queryName: SampleBarChartDataBuilder.CategoryColumn,
            type: ValueType.fromDescriptor({ text: true }),
            roles: {
                Category: true
            },
        },
        values: this.valuesCategory
    }
],
[
    {
        source: {
            displayName: "Measure",
            isMeasure: true,
            queryName: SampleBarChartDataBuilder.MeasureColumn,
            type: ValueType.fromDescriptor({ numeric: true }),
            roles: {
                Measure: true
            },
        },
        values: this.valuesMeasure
    },
], columnNames)

Em que this.valuesCategory é uma matriz de categorias:

public valuesCategory: string[] = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];

E this.valuesMeasure é uma matriz de medidas para cada categoria:

public valuesMeasure: number[] = [742731.43, 162066.43, 283085.78, 300263.49, 376074.57, 814724.34, 570921.34];

A versão final de visualData.ts contém o seguinte código:

import powerbi from "powerbi-visuals-api";
import DataView = powerbi.DataView;

import { testDataViewBuilder } from "powerbi-visuals-utils-testutils";
import { valueType } from "powerbi-visuals-utils-typeutils";
import ValueType = valueType.ValueType;

import TestDataViewBuilder = testDataViewBuilder.TestDataViewBuilder;

export class SampleBarChartDataBuilder extends TestDataViewBuilder {
  public static CategoryColumn: string = "category";
  public static MeasureColumn: string = "measure";
  public valuesCategory: string[] = [
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday",
    "Sunday",
  ];
  public valuesMeasure: number[] = [
    742731.43, 162066.43, 283085.78, 300263.49, 376074.57, 814724.34, 570921.34,
  ];

  public getDataView(columnNames?: string[]): DataView {
    let dataView: any = this.createCategoricalDataViewBuilder(
      [
        {
          source: {
            displayName: "Category",
            queryName: SampleBarChartDataBuilder.CategoryColumn,
            type: ValueType.fromDescriptor({ text: true }),
            roles: {
              category: true,
            },
          },
          values: this.valuesCategory,
        },
      ],
      [
        {
          source: {
            displayName: "Measure",
            isMeasure: true,
            queryName: SampleBarChartDataBuilder.MeasureColumn,
            type: ValueType.fromDescriptor({ numeric: true }),
            roles: {
              measure: true,
            },
          },
          values: this.valuesMeasure,
        },
      ],
      columnNames
    ).build();

    // there's client side computed maxValue
    let maxLocal = 0;
    this.valuesMeasure.forEach((item) => {
      if (item > maxLocal) {
        maxLocal = item;
      }
    });
    (<any>dataView).categorical.values[0].maxLocal = maxLocal;

    return dataView;
  }
}

agora você pode usar a classe SampleBarChartDataBuilder em seu teste de unidade.

A ValueType classe é definida no pacote powerbi-visuals-utils-testutils.

Adicione o pacote powerbi-visuals-utils-testutils às dependências. No arquivo package.json, localize a seção dependencies e adicione o seguinte código:

"powerbi-visuals-utils-testutils": "^2.4.1",

Chamar

npm install

para instalar o pacote powerbi-visuals-utils-testutils.

Agora, você pode executar o teste de unidade novamente. Você deve obter a seguinte saída:

> karma start

23 05 2017 16:19:54.318:WARN [watcher]: Pattern "E:/WORKSPACE/PowerBI/PowerBI-visuals-sampleBarChart/data/*.csv" does not match any file.
23 05 2017 16:19:58.333:WARN [karma]: No captured browser, open https://localhost:9876/
23 05 2017 16:19:58.346:INFO [karma]: Karma v1.3.0 server started at https://localhost:9876/
23 05 2017 16:19:58.346:INFO [launcher]: Launching browser Chrome with unlimited concurrency
23 05 2017 16:19:58.394:INFO [launcher]: Starting browser Chrome
23 05 2017 16:19:59.873:INFO [Chrome 58.0.3029 (Windows 10 0.0.0)]: Connected on socket /#NcNTAGH9hWfGMCuEAAAA with id 3551106
Chrome 58.0.3029 (Windows 10 0.0.0): Executed 1 of 1 SUCCESS (1.266 secs / 1.052 secs)

=============================== Coverage summary ===============================
Statements   : 56.72% ( 135/238 )
Branches     : 32.54% ( 41/126 )
Functions    : 66.67% ( 38/57 )
Lines        : 52.83% ( 112/212 )
================================================================================

O resumo mostra que a cobertura aumentou. Para saber mais sobre a cobertura de código atual, abra o arquivo coverage/html-report/index.html.

Screenshot of the browser window, which shows the HTML code coverage report.

Ou examine o escopo da pasta src:

Screenshot of the browser window, which shows the code coverage report for the visual dot ts file.

No escopo do arquivo, você pode exibir o código-fonte. Os utilitários coverage realçarão a linha em vermelho se determinadas linhas código não forem executadas durante os testes de unidade.

Screenshot of the visual source code, which shows that the lines of code that didn't run in unit tests are highlighted in red.

Importante

A cobertura de código não significa que você tenha boa cobertura de funcionalidade do visual. Um teste de unidade simples fornece uma cobertura de mais de 96% em src/barChart.ts.

Depuração

Para depurar seus testes por meio do console do navegador, altere o valor de singleRun em karma.config.ts para false. Essa configuração manterá o navegador em execução quando ele for iniciado após a execução dos testes.

Seu visual é aberto no navegador Chrome.

Screenshot of the Chrome browser window, which shows the custom Power BI visual.

Quando seu visual está pronto, você pode enviá-lo para publicação. Para saber mais, confira Publicar visuais do Power BI no AppSource.