这是我第一次进行测试,并且获得了测试UI组件的技巧。现在,我正在尝试测试其中包含一些静态方法的类。它也包含参数。

见上课:

import UserInfoModel from '../models/UserInfo.model';
import ApiClient from './apiClient';
import ApiNormalizer from './apiNormalizer';
import Article from '../models/Article.model';
import Notification from '../models/Notification.model';
import Content from '../models/Link.model';

export interface ResponseData {
  [key: string]: any;
}

export default class ApiService {
  static makeApiCall(
    url: string,
    normalizeCallback: (d: ResponseData) => ResponseData | null,
    callback: (d: any) => any
  ) {
    return ApiClient.get(url)
      .then(res => {
        callback(normalizeCallback(res.data));
      })
      .catch(error => {
        console.error(error);
      });
  }


  static getProfile(callback: (a: UserInfoModel) => void) {
    return ApiService.makeApiCall(`profile`, ApiNormalizer.normalizeProfile, callback);
  }
}

我已经创建了一个通过的小测试,但是我不确定自己在做什么。
// @ts-ignore
import moxios from 'moxios';
import axios from 'axios';
import { baseURL } from './apiClient';
import { dummyUserInfo } from './../models/UserInfo.model';

describe('apiService', () => {
  let axiosInstance: any;

  beforeEach(() => {
    axiosInstance = axios.create();
    moxios.install();
  });

  afterEach(() => {
    moxios.uninstall();
  });

  it('should perform get profile call', done => {
    moxios.stubRequest(`${baseURL.DEV}profile`, {
      status: 200,
      response: {
        _user: dummyUserInfo
      }
    });

    axiosInstance
      .get(`${baseURL.DEV}profile`)
      .then((res: any) => {
        expect(res.status).toEqual(200);
        expect(res.data._user).toEqual(dummyUserInfo);
      })
      .finally(done);
  });
});

我正在使用moxios测试axios的内容-> https://github.com/axios/moxios

那么哪种方法可以测试此类的正确方法呢?

最佳答案

介绍

Unit tests是由软件开发人员编写和运行的自动化测试,以确保应用程序的某个部分符合其设计并按预期运行。就像我们在谈论面向对象的编程一样,一个单元通常是一个完整的接口(interface),例如一个类,但是可以是一个单独的方法。

单元测试的目的是隔离程序的每个部分,并表明各个部分是正确的。因此,如果我们考虑您的ApiService.makeApiCall函数:

  static makeApiCall(
    url: string,
    normalizeCallback: (d: ResponseData) => ResponseData | null,
    callback: (d: any) => any
  ) {
    return ApiClient.get(url)
      .then((res: any) => {
        callback(normalizeCallback(res.data));
      })
      .catch(error => {
        console.error(error);
      });
  }

我们可以看到它有一个外部资源调用ApiClient.get,应该是mocked。在这种情况下,模拟HTTP请求并不是完全正确的,因为ApiService不会直接利用它们,在这种情况下,您的单元变得比预期的要宽一些。

模拟

Jest框架提供了mocking的强大机制,Omair Nabiel的示例是正确的。但是,我不仅希望对具有预定义数据的函数进行 stub ,而且还要检查被 stub 的函数是否被调用了预期的次数(因此请使用模拟的真实本质)。因此,完整的模拟示例如下所示:

/**
* Importing `ApiClient` directly in order to reference it later
*/
import ApiClient from './apiClient';

/**
* Mocking `ApiClient` with some fake data provider
*/
const mockData = {};

jest.mock('./apiClient', function () {

  return {
    get: jest.fn((url: string) => {
      return Promise.resolve({data: mockData});
    })
  }
});

这允许向您的测试示例添加其他断言:

it('should call api client method', () => {
  ApiService.makeApiCall('test url', (data) => data, (res) => res);
  /**
  * Checking `ApiClient.get` to be called desired number of times
  * with correct arguments
  */
  expect(ApiClient.get).toBeCalledTimes(1);
  expect(ApiClient.get).toBeCalledWith('test url');
});

正面测试

因此,只要我们弄清楚了什么以及如何模拟数据,就让我们找出应该测试的内容。好的测试应涵盖two situations:积极测试-通过提供有效数据来测试系统,负面测试-通过提供无效数据来测试系统。以我的拙见,应该添加第三个分支-边界测试-测试,重点放在要测试的软件的边界或限制条件上。如果您对其他类型的测试感兴趣,请引用此Glossary

因此,makeApiCall方法的正向测试流程应调用normalizeCallbackcallback方法,我们可以按以下方式编写此测试(但是,有多种方法可以给猫换皮):

  it('should call callbacks consequently', (done) => {
    const firstCallback = jest.fn((data: any) => {
      return data;
    });
    const secondCallback = jest.fn((data: any) => {
      return data;
    });
    ApiService.makeApiCall('test url', firstCallback, secondCallback)
      .then(() => {
        expect(firstCallback).toBeCalledTimes(1);
        expect(firstCallback).toBeCalledWith(mockData);

        expect(secondCallback).toBeCalledTimes(1);
        expect(secondCallback).toBeCalledWith(firstCallback(mockData));
        done();
      });
  });

请注意此测试中的几件事:
-由于此测试的异步性质,我正在使用done回调来让测试知道该测试已完成
-我使用的mockData变量模拟了ApiClient.get的数据,因此我检查了回调是否获得了正确的值
-mockData和类似变量应从mock开始。否则,Jest将不允许将其从模拟scope中删除

阴性测试

测试的负面方式看起来很相似。 ApiClient.get方法应该抛出错误,而ApiService应该对其进行处理并放入console中。另外,我正在检查没有回调被调用。

import ApiService from './api.service';

const mockError = {message: 'Smth Bad Happened'};

jest.mock('./apiClient', function () {

  return {
    get: jest.fn().mockImplementation((url: string) => {
      console.log('error result');
      return Promise.reject(mockError);
    })
  }
});

describe( 't1', () => {
  it('should handle error', (done) => {
    console.error = jest.fn();

    const firstCallback = jest.fn((data: any) => {
      return data;
    });
    const secondCallback = jest.fn((data: any) => {
      return data;
    });
    ApiService.makeApiCall('test url', firstCallback, secondCallback)
      .then(() => {
        expect(firstCallback).toBeCalledTimes(0);
        expect(secondCallback).toBeCalledTimes(0);
        expect(console.error).toBeCalledTimes(1);
        expect(console.error).toBeCalledWith(mockError);
        done();
      });
  });
});

边界测试

边界测试可能在您的情况下争论不休,但是只要(根据您的类型定义normalizeCallback: (d: ResponseData) => ResponseData | null)第一个回调可以返回null,这是一种很好的做法,检查是否已成功转移到第二个回调而没有任何错误或异常。我们可以稍微重写一下第二个测试:

it('should call callbacks consequently', (done) => {
    const firstCallback = jest.fn((data: any) => {
      return null;
    });
    const secondCallback = jest.fn((data: any) => {
      return data;
    });
    ApiService.makeApiCall('test url', firstCallback, secondCallback)
      .then(() => {
        expect(firstCallback).toBeCalledTimes(1);
        expect(firstCallback).toBeCalledWith(mockData);
        expect(secondCallback).toBeCalledTimes(1);
        done();
      });
  });

测试异步代码

关于测试异步代码,您可以阅读全面的文档here。主要思想是,当您拥有异步运行的代码时,Jest需要知道它所测试的代码何时完成,然后才能继续进行另一项测试。 Jest提供了三种方法来实现此目的:
  • 通过回调
    it('the data is peanut butter', done => {
      function callback(data) {
        expect(data).toBe('peanut butter');
        done();
      }
    
      fetchData(callback);
    });
    

    Jest将等待直到完成的回调被调用,然后再完成测试。如果从不调用done(),则测试将失败,这就是您想要发生的情况。
  • 通过 promise

    如果您的代码使用Promise,则有一种更简单的方式来处理异步测试。只需从测试中返回一个 promise ,Jest将等待该 promise 解决。如果 promise 被拒绝,则测试将自动失败。
  • async/await语法

    您可以在测试中使用asyncawait。要编写异步测试,只需在传递给测试的函数前面使用async关键字即可。
    it('the data is peanut butter', async () => {
    const data = await fetchData();
    expect(data).toBe('peanut butter');
    });
    

  • 例子

    在这里,您可以找到可立即使用的代码示例
    https://github.com/SergeyMell/jest-experiments
    请让我知道是否有什么不清楚的地方。

    更新(29.08.2019)

    关于你的问题



    根据documentation的说法,Jest会自动将jest.mock调用提升到模块的顶部(在导入之前)。看来您可以改用setMockdoMock,但是,有issues带有这种 mock 的方式,开发人员有时会遇到这种情况。可以使用require而不是import和其他技巧(请参阅this article)来覆盖它们,但是我不喜欢这种方式。

    在这种情况下,对我而言正确的方法是拆分模拟的定义和实现,因此您声明该模块将像这样被模拟

    jest.mock('./apiClient', function () {
      return {
        get: jest.fn()
      }
    });
    

    但是,根据测试范围,模拟功能的实现有所不同:

    describe('api service success flow', () => {
    
      beforeAll(() => {
        //@ts-ignore
        ApiClient.get.mockImplementation((url: string) => {
          return Promise.resolve({data: mockData});
        })
      });
    
      ...
    });
    
    describe('api service error flow', () => {
    
      beforeAll(() => {
        //@ts-ignore
        ApiClient.get.mockImplementation((url: string) => {
          console.log('error result');
          return Promise.reject(mockError);
        })
      });
    
      ...
    });
    

    据我所知,这将允许您将所有与api service相关的流存储在单个文件中。
    我已经用api.spec.ts更新了github example,它实现了上述所有功能。请看一下。

    关于javascript - 如何测试包含导入的异步方法的类?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/57471357/

    10-09 05:40
    查看更多