Как выполнить модульное тестирование «не автономных функций»?

avatar
TommyLeong
9 августа 2021 в 06:56
129
2
0

У меня есть знания в области модульного тестирования автономных функций, таких как вспомогательные классы, но как мне работать с функциями, которые не являются автономными, обычно в файле класса, который имеет несколько проверок и результатов?

Пример ниже, показывающий проверку нескольких условий и ответ в разных результатах.

  • Вызывать ли функции valueCheck и proceedApiCheck в моем тестовом примере? Но есть разные сценарии или действия, которые мне не нужны в тесте. (например, setState/навигация)
  • Написать ли мне новую функцию valueCheck и proceedApiCheck в моем тестовом примере? Но это будет означать, что у меня есть 2 разные логики в моем коде. Когда-нибудь, если я изменю свою логику в приложении, мой тестовый пример не завершится ошибкой, поскольку он ссылается на старую логику.

Кто-нибудь из вас может пролить свет на это?

Пример

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


}
Источник
Drew Reese
9 августа 2021 в 08:29
0

Ваш вопрос кажется субъективным и слишком открытым, неясно, что вы действительно хотите получить. Попробуйте предоставить пример модульного теста и сузить фокус вашего запроса. Я предлагаю также изучить React-Testing-Library для модульного тестирования пользовательского интерфейса.

TommyLeong
9 августа 2021 в 08:39
0

Привет @DrewReese, спасибо за ваши комментарии. Я обновил свой вопрос, чтобы он был очень конкретным при тестировании не автономных функций. Спасибо, что указали. Мой вопрос действительно заключается в том, как UnitTest всегда может ссылаться на 1 часть логики, поэтому при обновлении логики приложения нам не требуется обновлять логику UnitTest. Это мое замешательство.

Drew Reese
9 августа 2021 в 08:41
0

Я не уверен, что понимаю ваше замешательство. Вы экспортируете/импортируете функции/компоненты, которые хотите протестировать.

Drew Reese
9 августа 2021 в 08:44
0

Ах, я думаю, что понимаю путаницу... именно поэтому я указал вам на RTL.... суть просто в том, что вы не "добираетесь" до компонентов React и не касаетесь внутренних реализаций при модульном тестировании, а скорее что вы взаимодействуете с компонентом через его API, то есть props и пользовательский интерфейс, так же, как другие компоненты и пользователи. Если вам нужно или можно, вы можете вытащить определенный фрагмент кода в служебную функцию и протестировать изолированно, хотя это невозможно, если они связаны с обновлениями состояния компонентов.

Horst
9 августа 2021 в 10:21
0

@TommyLeong Чтобы выполнить модульное тестирование, напишите тестируемый код для любого модуля, который вам нужно охватить. В вашем случае эти две функции не являются хорошими единицами, а Screen1. попробуйте написать единичный случай для Screen1 с фиктивными данными, которые вызовут valueCheck

TommyLeong
9 августа 2021 в 11:47
0

Привет @Horst, вы предлагаете мне выполнить модульный тест непосредственно на Screen1 вместо двух функций?

Ответы (2)

avatar
Jared Smith
10 августа 2021 в 02:32
1

Вы сделали ценное открытие:

Самый простой способ написания кода не обязательно является самым надежным способом написания кода.

Эта досадная проблема усугубляется тем, как большинство людей используют классы: неявная ссылка this позволяет очень легко сделать слишком много в одном методе, не говоря уже об одном классе. И это привело вас ко второму ценному открытию:

.

Одна из самых ценных вещей в изолированных модульных тестах заключается в том, что они дают вам отзывы о вашем проекте.

Здесь трудность выделения теста говорит вам о том, что вы соединили отдельные проблемы таким образом, что их трудно разделить. У вас есть метод с семью (7!!) различными точками выхода, и теперь вы застряли, потому что это много насмешек, чтобы попытаться вызвать соответствующую логику, чтобы убедиться, что вы нажмете все из них .

Рассмотрите следующий вариант:

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. Единственная вещь, которую делает continueApiCheck, — это получение значения из логической функции и переход к нужному экрану, а также простая имитация для теста.

Вы могли бы, если вы действительно занимаетесь классами, также сделать все эти функции статическими методами вашего класса, но важно то, что почти ни один из ваших тестов не нуждается в сложных макетах, и это должно быть гораздо более очевидно, что такое пути кода и как их тестировать.

Высшая практика разработки программного обеспечения, которой вы хотите придерживаться (как указано @DrewReese в комментариях), — это принцип единой ответственности, который гласит, что функции/методы должны только выполнить одну логическую операцию. Валидатор только проверяет, условное только выполняет отправку, функция, которая получает данные из внешнего источника, должна только получать данные (а не воздействовать на них) и т. д.

TommyLeong
10 августа 2021 в 07:25
0

Я вижу, короче говоря, статический метод должен использоваться для функций в классах, поэтому UT можно сделать правильно? Я предполагаю, что мне нужно как можно больше «отделить мои функции и заставить их возвращать значение», поэтому я могу лучше продолжить работу с UT. Я правильно тебя понял?

Jared Smith
10 августа 2021 в 10:46
2

@TommyLeong точно. Вам не необходимо использовать статические методы: вы можете использовать простые функции, как это сделал я, и это больше похоже на стиль. Но вы правильно поняли важную часть: вы хотите, чтобы как можно больше простых функций/методов выполняли одно действие и возвращали значения (а не изменяли this). Их намного проще тестировать, потому что они не имеют 8 различных путей кода через них (вам действительно нужна одна, не более двух точек выхода для функции), и они зависят только от своих входных данных, а не от эфемерного состояния объекта.

Drew Reese
10 августа 2021 в 16:06
1

@JaredSmith Я большой поклонник принципа единой ответственности, когда речь идет о ремонтопригодности и тестируемости кода. Вы обязательно должны выделить этот аспект!

TommyLeong
12 августа 2021 в 14:05
1

Спасибо @JaredSmith за то, что поделились мыслями и направлением. Постараюсь разбить функции на модульный уровень для тестирования и повторного использования.

avatar
Horst
10 августа 2021 в 02:10
0

если вы считаете необходимым выполнить модульный тест для 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

TommyLeong
10 августа 2021 в 07:26
0

Я понимаю, что ваша точка зрения очень похожа на то, что поделился @Jared. В основном, разделение функций на более мелкие единицы, и я буду называть это функциями в своем UT. Верный?