Leer en inglés

Compartir a través de


Tutorial: Incorporación de pruebas unitarias para proyectos visuales de Power BI

En este artículo se describen los aspectos básicos de la escritura de pruebas unitarias para los objetos visuales de Power BI, incluido cómo:

  • Configurar el marco de pruebas de ejecutor de pruebas de JavaScript de Karma, Jasmine.
  • Usa el paquete powerbi-visuals-utils-testutils.
  • Utiliza simulaciones y falsificaciones para ayudar a simplificar las pruebas unitarias de elementos visuales de Power BI.

Prerrequisitos

  • Un proyecto de objetos visuales de Power BI instalado
  • Un entorno de Node.js configurado.

En los ejemplos de este artículo, se usa el objeto visual de gráfico de barras para las pruebas.

Instalación y configuración del ejecutor de pruebas de JavaScript de Karma y Jasmine

Agregue las bibliotecas necesarias al archivo package.json en la sección devDependencies:

JSON
"@types/jasmine": "^5.1.5",
"@types/karma": "^6.3.9",
"coverage-istanbul-loader": "^3.0.5",
"jasmine": "^5.5.0",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1",
"karma-coverage-istanbul-reporter": "^3.0.3",
"karma-jasmine": "^5.1.0",
"karma-junit-reporter": "^2.0.1",
"karma-sourcemap-loader": "^0.4.0",
"karma-typescript": "^5.5.4",
"karma-typescript-preprocessor": "^0.4.0",
"karma-webpack": "^5.0.1",
"playwright-chromium": "^1.49.0",
"powerbi-visuals-api": "~5.11.0",
"powerbi-visuals-tools": "^5.6.0",
"powerbi-visuals-utils-testutils": "6.1.1",
"powerbi-visuals-utils-typeutils": "6.0.3",
"style-loader": "^4.0.0",
"ts-loader": "~9.5.1"

Para obtener más información sobre package.json, consulte la descripción en npm-package.json.

Guarde el archivo package.json y ejecute el siguiente comando en la ubicación del archivo package.json:

Símbolo del sistema de Windows
npm install

El administrador de paquetes instala todos los paquetes nuevos que se agregan a package.json.

Para ejecutar pruebas unitarias, configure el ejecutor de pruebas y la configuración de webpack.

El código siguiente es un ejemplo del archivo test.webpack.config.js:

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

module.exports = {
    devtool: 'source-map',
    mode: 'development',
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            },
            {
                test: /\.json$/,
                loader: 'json-loader'
            },
            {
                test: /\.tsx?$/i,
                enforce: 'post',
                include: path.resolve(__dirname, 'src'),
                exclude: /(node_modules|resources\/js\/vendor)/,
                loader: 'coverage-istanbul-loader',
                options: { esModules: true }
            },
            {
                test: /\.less$/,
                use: [
                    {
                        loader: 'style-loader'
                    },
                    {
                        loader: 'css-loader'
                    },
                    {
                        loader: 'less-loader',
                        options: {
                            lessOptions: {
                                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
        })
    ]
};

El código siguiente es un ejemplo del archivo test.tsconfig.json:

JSON
{
  "compilerOptions": {
    "allowJs": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es2022",
    "sourceMap": true,
    "outDir": "./.tmp/build/",
    "sourceRoot": "../../src/",
    "moduleResolution": "node",
    "declaration": true,
    "lib": [
      "es2022",
      "dom"
  ]
  },
  "files": [
    "./test/visualTest.ts"
  ],
  "include": [
      "src/*.ts"
  ]
}

El código siguiente es un ejemplo del archivo karma.conf.ts:

TypeScript
"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("playwright-chromium").chromium.executablePath();

module.exports = (config) => {
    config.set({
        mode: "development",
        browserNoActivityTimeout: 100000,
        browsers: ["ChromeHeadless"], // or specify Chrome to use the locally installed Chrome browser
        colors: true,
        frameworks: ["jasmine", "webpack"],
        reporters: [
            "progress",
            "junit",
            "coverage",
            "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: [
            testRecursivePath,
            {
                pattern: srcOriginalRecursivePath,
                included: false,
                served: true
            },
            {
                pattern: './capabilities.json',
                watched: false,
                served: true,
                included: false
            }
        ],
        preprocessors: {
            [testRecursivePath]: ["webpack"]
        },
        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: {
            type: "html",
            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"
        }
    });
};

Si es necesario, puede modificar esta configuración.

El código de karma.conf.js contiene las siguientes variables:

  • testRecursivePath: localiza el código de prueba.

  • srcOriginalRecursivePath: localiza el código fuente de tu visual.

  • coverageFolder: determina dónde se va a crear el informe de cobertura.

El archivo de configuración incluye las siguientes propiedades:

  • singleRun: true: las pruebas se ejecutan en un sistema de integración continua (CI) o se pueden ejecutar una vez. Puede cambiar la configuración a false para depurar las pruebas. El marco Karma mantiene el explorador en ejecución para que pueda usar la consola para la depuración.

  • files: [...]: en esta matriz, puede especificar los archivos que se van a cargar en el explorador. Los archivos que se cargan suelen ser archivos de origen, casos de prueba y bibliotecas (como Jasmine o utilidades de prueba). Puede agregar más archivos según sea necesario.

  • preprocessors: en esta sección, configurará las acciones que se ejecutan antes de que se ejecuten las pruebas unitarias. Las acciones pueden precompilar TypeScript en JavaScript, preparar archivos de mapa de código fuente y generar un informe de cobertura de código. Puede deshabilitar coverage al depurar las pruebas. coverage genera más código para las pruebas de cobertura del código, lo que complica la depuración de las pruebas.

Para obtener descripciones de todas las configuraciones de Karma, vaya a la página del archivo de configuración de Karma.

Para mayor comodidad, puede agregar un comando de prueba a scripts en package.json:

JSON
{
    "scripts": {
        "pbiviz": "pbiviz",
        "start": "pbiviz start",
        "package": "pbiviz package",
        "pretest": "pbiviz package --resources --no-minify --no-pbiviz",
        "test": "karma start",
        "debug": "karma start --single-run=false --browsers=Chrome"
    }
    ...
}

Ya puede empezar a escribir las pruebas unitarias.

Verificar el elemento DOM del visual

Para probar el objeto visual, cree primero una instancia del objeto visual.

Creación de un generador de instancias visuales

Agregue un archivo visualBuilder.ts a la carpeta de prueba mediante el código siguiente:

TypeScript
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(): SVGElement | null {
    return this.element.querySelector("svg.barChart");
  }
}

El método build crea una instancia del objeto visual. mainElement es un método de obtención que devuelve una instancia de un elemento raíz del modelo de objetos de documento (DOM) en tu visualización. El captador es opcional, pero facilita la escritura de la prueba unitaria.

Ya tiene una compilación de una instancia de su objeto visual. Vamos a escribir el caso de prueba. El caso de prueba de ejemplo comprueba los elementos SVG que se crean cuando tu visualización se muestra.

Creación de un archivo TypeScript para escribir casos de prueba

Agregue un archivo visualTest.ts para los casos de prueba mediante el código siguiente:

TypeScript
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(document.body.contains(visualBuilder.mainElement)).toBeTruthy();
    });
  });
});

Se denominan varios métodos Jasmine:

  • describe: describe un caso de prueba. En el contexto del marco jasmine, describe a menudo describe un conjunto o grupo de especificaciones.

  • beforeEach: se llama antes de cada llamada del método it, que está definido por el método describe.

  • it: define una sola especificación. El método it debe contener uno o varios expectations.

  • expect: Crea una expectativa para una especificación. Una especificación tiene éxito si todas las expectativas se cumplen sin fallos.

  • toBeInDOM: uno de los métodos buscadores de coincidencias. Para obtener más información sobre los buscadores de coincidencias, vea Jasmine Namespace: matchers (Espacio de nombres de Jasmine: buscadores de coincidencias).

Para obtener más información sobre Jasmine, consulte la documentación del marco de Jasmine en la página.

Adición de datos estáticos para pruebas unitarias

Cree el archivo visualData.ts en la carpeta de prueba usando el siguiente código:

TypeScript
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 dataView: 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;
  }
}

La clase SampleBarChartDataBuilder extiende TestDataViewBuilder e implementa el método abstracto getDataView.

Al colocar datos en cubos de campo de datos, Power BI genera un objeto de categoría dataview basado en los datos.

Captura de pantalla de Power BI, que muestra que los cubos de campos de datos están vacíos.

En las pruebas unitarias, no tiene acceso a las funciones principales de Power BI que normalmente se usan para reproducir los datos. pero debe asignar los datos estáticos al objeto dataview categórico. Usa la clase TestDataViewBuilder para mapear tus datos estáticos.

Para obtener más información sobre la asignación de vistas de datos, vea DataViewMappings.

En el método getDataView, llame al método createCategoricalDataViewBuilder con los datos.

En el archivo visual capabilities.json de sampleBarChart, tenemos objetos dataRoles y dataViewMapping.

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

Para generar la misma asignación, debe establecer los siguientes parámetros en el método createCategoricalDataViewBuilder:

TypeScript
([
    {
        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)

Donde this.valuesCategory es una matriz de categorías:

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

Y this.valuesMeasure es una matriz de medidas para cada categoría:

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

La versión final de visualData.ts contiene el código siguiente:

ts
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;
  }
}

La clase ValueType se define en el paquete powerbi-visuals-utils-typeutils.

Ahora puedes ejecutar la prueba unitaria.

Iniciar pruebas unitarias

Esta prueba comprueba que el elemento SVG raíz del objeto visual existe cuando se ejecuta el objeto visual. Para ejecutar la prueba unitaria, escriba el siguiente comando en la herramienta de línea de comandos:

Símbolo del sistema de Windows
npm run test

karma.js ejecuta el caso de prueba en el explorador Chrome.

Captura de pantalla del explorador Chrome, que muestra que karma dot js está ejecutando el caso de prueba.

Nota

Debe instalar Google Chrome localmente.

En la ventana de la línea de comandos, obtendrá la siguiente salida:

Símbolo del sistema de Windows
> karma start

Webpack bundling...
assets by status 8.31 KiB [compared for emit]
  assets by path ../build/test/*.ts 1020 bytes
    asset ../build/test/visualData.d.ts 512 bytes [compared for emit]
    asset ../build/test/visualBuilder.d.ts 499 bytes [compared for emit]
    asset ../build/test/visualTest.d.ts 11 bytes [compared for emit]
  assets by path ../build/src/*.ts 6.67 KiB
    asset ../build/src/barChart.d.ts 4.49 KiB [compared for emit]
    asset ../build/src/barChartSettingsModel.d.ts 2.18 KiB [compared for emit]
  asset visualTest.3941401795.js 662 bytes [compared for emit] (name: visualTest.3941401795) 1 related asset
assets by status 2.48 MiB [emitted]
  asset commons.js 2.48 MiB [emitted] (name: commons) (id hint: commons) 1 related asset
  asset runtime.js 6.48 KiB [emitted] (name: runtime) 1 related asset
Entrypoint visualTest.3941401795 2.48 MiB (2.34 MiB) = runtime.js 6.48 KiB commons.js 2.48 MiB visualTest.3941401795.js 662 bytes 3 auxiliary assets        
webpack 5.97.0 compiled successfully in 3847 ms
04 12 2024 11:01:19.255:INFO [karma-server]: Karma v6.4.4 server started at http://localhost:9876/
04 12 2024 11:01:19.257:INFO [launcher]: Launching browsers ChromeHeadless with concurrency unlimited
04 12 2024 11:01:19.277:INFO [launcher]: Starting browser ChromeHeadless
04 12 2024 11:01:20.634:INFO [Chrome Headless 131.0.0.0 (Windows 10)]: Connected on socket QYSj9NyHQ14QjFBoAAAB with id 9616879
Chrome Headless 131.0.0.0 (Windows 10): Executed 1 of 1 SUCCESS (0.016 secs / 0.025 secs)
TOTAL: 1 SUCCESS
TOTAL: 1 SUCCESS

=============================== Coverage summary ===============================
Statements   : 66.07% ( 187/283 )
Branches     : 34.88% ( 45/129 )
Functions    : 52.85% ( 37/70 )
Lines        : 65.83% ( 185/281 )
================================================================================

Para obtener más información sobre la cobertura de código actual, abra el archivo coverage/html-report/index.html.

Recorte de pantalla de la ventana del explorador, que muestra el informe de cobertura de código para el archivo visual punto ts.

En el ámbito del archivo, puede ver el código fuente. Las utilidades de coverage resaltan la fila en rojo si ciertas líneas de código no se ejecutan durante las pruebas unitarias.

Captura de pantalla del código fuente visual, que muestra que las líneas de código que no se ejecutaron en pruebas unitarias están resaltadas en rojo.

Importante

La cobertura de código no implica que tenga una buena cobertura de funcionalidad del objeto visual. Una prueba unitaria simple proporciona más del 96 % de cobertura en src/barChart.ts.

Depuración

Para depurar las pruebas mediante la consola del explorador, cambie el valor de singleRun en el archivo karma.conf.ts a false. Esta configuración mantendrá el explorador en ejecución cuando se inicie el explorador después de ejecutar las pruebas.

El objeto visual se abre en el explorador Chrome.

Captura de pantalla de la ventana del explorador Chrome, que muestra el objeto visual personalizado de Power BI.

Cuando el visual esté listo, puedes enviarlo para su publicación. Para más información, consulte Publicación de objetos visuales de Power BI en AppSource.