【问题标题】:How do I unit test "non standalone functions"?如何对“非独立功能”进行单元测试?
【发布时间】:2021-10-12 21:43:19
【问题描述】:

我了解单元测试独立函数(如帮助程序类)的知识,但我如何处理非独立函数(通常在具有多个验证和结果的类文件中)?

下面的示例显示了多个条件检查和不同结果的响应。

  • 我是否在我的测试用例中调用valueCheckproceedApiCheck 函数?但是在测试中我不需要不同的场景或操作。 (例如,setState / navigating)
  • 我是否在我的测试用例中编写了valueCheckproceedApiCheck 的新函数?但这意味着我的代码中有两种不同的逻辑。有一天,如果我在应用程序中更改逻辑,我的测试用例不会失败,因为它引用的是旧逻辑。

你们中的任何人都可以对此有所了解吗?

示例

export class Screen1 extends React.Component {

    valueCheck = (value) => {
        if(value === 'abc'){
            this.setState({ isNavigating:true, transfer: true })
            this.proceedApiCheck(value)
        }
        if(value === '123'){
            this.setState({ isNavigating:true, transfer: false })
            this.proceedApiCheck(value)
        }
    }

    proceedApiCheck = async(value) =>{
        let data
        try{
            data = await FirstApi(value);
            this.setState(data)
        }catch(){
            this.navigateToScreen('Failure')
            return;
        }

        switch(data.name){
            case 'fake adidas':
                this.navigateToScreen('Failure')
                return;
            case 'fake nike':
                this.navigateToScreen('Failure')
                return; 
        }
        
        try{
            const result = await secondApi(data.price);

            switch(result.currency){
                case 'EURO':
                    this.navigateToScreen('Euro')
                case 'Pound':
                    this.navigateToScreen('Pound')
                default: 
                    this.navigateToScreen('Dollar')
            }
        }catch(){
            this.navigateToScreen('Failure')
            return;
        }
    }


}

【问题讨论】:

  • 您的问题似乎很主观而且过于开放,不清楚您真正想要回答什么。尝试提供一个示例单元测试用例并缩小查询的重点。我还建议您查看React-Testing-Library 进行 UI 单元测试。
  • 嗨@DrewReese,感谢您的cmets。我已经更新了我的问题,使其在测试非独立功能时非常具体。谢谢你指出。我的查询实际上是 UnitTest 如何始终引用 1 条逻辑,因此在更新应用程序逻辑时,不需要我们更新 UnitTest 逻辑。这就是我的困惑。
  • 我不确定我是否理解您的困惑。您导出/导入要进行单元测试的功能/组件。
  • 啊,我想我理解这种困惑......这也是我向你指出 RTL 的原因......要点很简单,你不要“接触”到 React 组件并触摸单元测试时的内部实现,而是通过它的 API 与组件交互,即props 和 UI,与其他组件和用户一样。如果您需要或可以,您可以将特定代码块提取到实用程序函数中并单独测试,但如果它们与组件状态更新耦合,则这是不可能的。
  • @TommyLeong 要进行单元测试,请为您需要涵盖的任何单元编写可测试的代码。在您的情况下,两个函数不是好的单位,而是Screen1。尝试使用将调用 valueCheck 的模拟数据为 Screen1 编写单元案例

标签: javascript reactjs react-native jestjs


【解决方案1】:

您有一个有价值的发现:

最直接的代码编写方式不一定是最稳健的代码编写方式。

大多数人使用类的方式加剧了这个不幸的问题:隐含的this 引用使得很容易在一个方法中做太多事情,更不用说一个类了。这使您获得了第二个有价值的发现:

关于独立单元测试的最有价值的事情之一是它们为您的设计提供反馈

在这里,隔离测试的困难在于告诉您,您以一种难以梳理的方式耦合了不同的关注点。你有一个方法有七个 (7!!) 不同的退出点,现在你被卡住了,因为这是一个 lot 的模拟,试图触发适当的逻辑以确保你击中所有他们。

考虑以下替代方案:

const FAIL = {}; // could also use Symbol() here, any unique ref
const BAD_NAMES = ['fake whatever'];
async function apiCall1() {
  const resp = await fetch(someURL);
  return resp.json();
}

function validate1(data) {
  return data?.name === undefined || BAD_NAMES.includes(data?.name) 
    ? FAIL 
    : data.name;
}

// You can imagine what validation and fetching look like for
// the second API call

function processData(data) {
  switch(data.currency){
    case 'EURO':
      return 'Euro';
    case 'Pound':
      return 'Pound';
    default: 
      return 'Dollar';
  }
}

async function doTheThing() { // use a better name IRL
  try {
    const first = await apiCall1();
    const data = validate1(first);
    if (data === FAIL) throw new Error('whatever');
    
    const second = await apiCall2(data.whatever);
    const data2 = validate2(second);
    if (data2 === FAIL) throw new Error('something else');

    // process data we now know is good.
    return processData(data);
  } catch (err) {
    console.error(err);
    return 'Failure';
  }
}

class Screen1 extends React.Component {
  async proceedApiCheck () {
    const nextScreen = await doTheThing();
    this.navigateToScreen(nextScreen);
  }
}
      
  1. 这里我们只在一个地方返回失败屏幕触发器。函数都有明确的出口。
  2. 有一个方便的功能可以取消装箱请求(或隐藏 xhr 的详细信息,如果您这样做的话)。
  3. 所有验证和业务逻辑都是可独立测试的。它也是在一个地方,而不是通过冗长的方法传播到各个地方。
  4. 所有逻辑都在返回值的普通函数中,除了fetch 之外不需要任何模拟。
  5. proceedApiCheck 所做的唯一事情是从逻辑函数中获取值并导航到正确的屏幕,并且很容易模拟测试。

如果你真的喜欢类,你也可以让你的类的所有这些函数成为静态方法,但重要的是你的测试几乎不需要复杂的模拟,它应该是更明显的是代码路径是什么以及如何测试它们。

您想要遵守的高风险软件工程实践(正如 @DrewReese 在 cmets 中指出的那样)是 Single Responsibility Principle,它指出函数/方法应该执行一个合乎逻辑的操作手术。验证器仅进行验证,条件仅进行调度,从外部源获取数据的函数应仅获取数据(而不是对其进行操作),等等。

【讨论】:

  • 我明白了,简而言之,静态方法应该用于类中的函数,因此可以完成 UT 对吗?我想我需要尽可能地“解耦我的函数并让它们返回值”,因此我可以更好地继续使用 UT。我说对了吗?
  • @TommyLeong 完全正确。您不需要需要使用静态方法:您可以像我一样使用普通函数,这更像是一种风格。但是你得到了正确的重要部分,你希望尽可能地拥有简单的函数/方法来做一件事并返回值(而不是修改this)。它们更容易进行单元测试,因为它们没有 8 个不同的代码路径通过它们(你真的想要一个,一个函数不超过两个退出点)并且它们只依赖于它们的输入而不是短暂的对象状态。
  • @JaredSmith 在代码可维护性和可测试性方面,我是Single Responsibility Principle 的忠实拥护者。你绝对应该强调这方面!
  • 感谢@JaredSmith 分享想法和方向。将尝试将功能分解为单元级别以进行测试和重用。
【解决方案2】:

如果您认为有必要对valueCheck 进行单元测试(在实际情况下可能是一个复杂的函数),请将其移出类并使其可单独测试。那么它应该是这样的:

function valueCheck(value) {
switch(value) {
case 'abc':
    return {shouldProceedApiCheck: true, newState:{ isNavigating:true, transfer: true }}
case '123':
    return {shouldProceedApiCheck: true, { isNavigating:true, transfer: false }}
default:
    return {shouldProceedApiCheck: false, newState: {} }
}

.....

class Screen1 extends React.Component {
....
whenToCall = ()=>{
    const {newState, shouldProceedApiCheck} = valueCheck(value)
    this.setState(valueCheck(newState), ()=>{
        if(shouldProceedApiCheck) {
          this.proceedApiCheck(value)
        }
    })
}
....
}

延伸阅读:https://medium.com/front-end-weekly/making-testable-javascript-code-2a71afba5120

【讨论】:

  • 我理解您的观点与@Jared 分享的内容非常相似。基本上将函数拆分为更小的单元,我将在我的 UT 中调用此函数。对吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-03-03
  • 2020-11-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-29
相关资源
最近更新 更多