Как избежать ловушек модульного тестирования с высоким покрытием кода с помощью TDD

Как избежать ловушек модульного тестирования с высоким покрытием кода с помощью TDD

4 февраля 2023 г.

Высокое покрытие кода модульными тестами часто рассматривается как ключевой показатель качества кода, и на то есть веские причины. Это демонстрирует, что значительная часть кодовой базы была протестирована и с меньшей вероятностью содержит ошибки. Однако, если вы просто видите цифры, как вы можете быть уверены, что ваша бизнес-логика покрыта? Или что ваш код легко поддерживать?

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

Две наиболее распространенные ошибки высокого охвата кода:

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

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

Как избежать этих ошибок?

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

* Пишите тесты, охватывающие все аспекты бизнес-логики, включая сложные части. Это обеспечит тестирование наиболее важных частей кодовой базы. * Используйте четкие и описательные названия тестов и утверждений. Это облегчит понимание того, что тест проверяет и почему он не пройден. * Следуйте принципам разработки через тестирование (TDD). Этот подход побуждает разработчиков писать тесты перед написанием кода, что может помочь гарантировать, что код хорошо протестирован с самого начала. * В обзорах кода уделяйте особое внимание модульным тестам. Легко ли понять тесты? Действительно ли они проверяют бизнес-правила? Или они используют бессмысленные значения только для того, чтобы убедиться, что они соответствуют условиям?

Чем может помочь TDD?

Давайте рассмотрим пример создания функции для проверки создания пароля. Команда разработчиков получит такое требование:

<цитата>

Новый пароль должен состоять не менее чем из 8 символов и содержать как минимум 1 строчную букву, 1 прописную букву, 1 цифру и специальный символ.

Впервые без TDD

Что в первую очередь сделает разработчик, который не следует TTD?

Найдя нужное регулярное выражение в Google, они напишут такую ​​функцию:

function validateNewPassword(newPassword) {
  let passwordRegex = new RegExp(
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&])[A-Za-zd@$!%*?&]{8,}$/
  )
  return passwordRegex.test(newPassword)
}

Затем время модульных тестов:

test('valid password', () => {
  expect(validateNewPassword('Abcdefg1!')).toBe(true)
})

test('invalid password', () => {
  expect(validateNewPassword('Abcdefgh1')).toBe(false)
})

Запустив тест, оба теста пройдут, и у нас будет 100% покрытие кода. Отлично, правда?

За исключением того, что с таким подходом есть две большие проблемы.

Во-первых, 100-процентное покрытие тестами было достигнуто путем тестирования только 1 из как минимум 5 сценариев использования неверных паролей.

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

Теперь давайте посмотрим, как можно было бы написать код по-другому, применив TDD.

Наше первое требование — минимальная длина =8. Давайте напишем для этого тест:

test('password minimum length', () => {
  expect(validateNewPassword('a'.repeat(7))).toBe(false)
})

И мы начинаем реализацию, применяя только этот первый тестовый проход:

function validateNewPassword(newPassword) {
  if (newPassword.length < 8) {
    return false
  }
  return true
}

Хорошо, теперь второе требование. Не менее одной заглавной буквы. Давайте напишем для этого тест:

test('password must contain at least one uppercase letter', () => {
  expect(validateNewPassword('abcdefgh1!')).toBe(false)
})

Чтобы этот тест прошел, мы можем добавить проверку заглавных букв в нашу функцию проверки:

function validateNewPassword(newPassword) {
  if (newPassword.length < 8) {
    return false
  }
  if (!newPassword.match(/[A-Z]/)) {
    return false
  }
  return true
}

Теперь строчные буквы:

test('password must contain at least one lowercase letter', () => {
  expect(validateNewPassword('ABCDEFGH1!')).toBe(false)
})

И мы добавляем проверку нижнего регистра в нашу функцию проверки:

function validateNewPassword(newPassword) {
  if (newPassword.length < 8) {
    return false
  }
  if (!newPassword.match(/[A-Z]/)) {
    return false
  }
  if (!newPassword.match(/[a-z]/)) {
    return false
  }
  return true
}

Теперь давайте перейдем к следующему требованию: хотя бы одна цифра.

test('password must contain at least one digit', () => {
  expect(validateNewPassword('Abcdefgh!')).toBe(false)
})

Чтобы этот тест прошел, мы можем добавить проверку цифр в нашу функцию проверки:

function validateNewPassword(newPassword) {
  if (newPassword.length < 8) {
    return false
  }
  if (!newPassword.match(/[A-Z]/)) {
    return false
  }
  if (!newPassword.match(/[a-z]/)) {
    return false
  }
  if (!newPassword.match(/d/)) {
    return false
  }
  return true
}

Наконец, давайте проверим последнее требование: наличие хотя бы одного специального символа.

test('password must contain at least one special symbol', () => {
  expect(validateNewPassword('Abcdefgh1')).toBe(false)
})

Чтобы этот тест прошел, мы можем добавить проверку специальных символов в нашу функцию проверки:

function validateNewPassword(newPassword) {
  if (newPassword.length < 8) {
    return false
  }
  if (!newPassword.match(/[A-Z]/)) {
    return false
  }
  if (!newPassword.match(/[a-z]/)) {
    return false
  }
  if (!newPassword.match(/d/)) {
    return false
  }
  if (!newPassword.match(/[@$!%*?&]/)) {
    return false
  }
  return true
}

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

Но кто-то может сказать: «Посмотрите на все эти «если», это не выглядит хорошо». Хорошо, давайте перепишем его для удобства чтения:

const LOWERCASE_REGEX = /[a-z]/
const UPPERCASE_REGEX = /[A-Z]/
const DIGIT_REGEX = /d/
const SPECIAL_CHAR_REGEX = /[@$!%*?&]/

function validateNewPassword(newPassword) {
  return (
    newPassword.length >= 8 &&
    LOWERCASE_REGEX.test(newPassword) &&
    UPPERCASE_REGEX.test(newPassword) &&
    DIGIT_REGEX.test(newPassword) &&
    SPECIAL_CHAR_REGEX.test(newPassword)
  )
}

И наш файл модульного теста будет выглядеть так:

describe('Password validation', () => {
  test('password minimum length is 8 characters', () => {
    // refactored after the first example to make sure the only reason for the test to fail is the length
    expect(validateNewPassword('Abcde1!')).toBe(false)
    expect(validateNewPassword('Abcdef1!')).toBe(true)
  })

  test('password must contain at least 1 uppercase letter', () => {
    expect(validateNewPassword('abcdefgh1!')).toBe(false)
    expect(validateNewPassword('Abcdefgh1!')).toBe(true)
  })

  test('password must contain at least 1 lowercase letter', () => {
    expect(validateNewPassword('ABCDEFGH1!')).toBe(false)
    expect(validateNewPassword('AbCdefgh1!')).toBe(true)
  })

  test('password must contain at least 1 digit', () => {
    expect(validateNewPassword('Abcdefgh!')).toBe(false)
    expect(validateNewPassword('Abcdefgh1!')).toBe(true)
  })

  test('password must contain at least 1 special symbol', () => {
    expect(validateNewPassword('Abcdefgh1')).toBe(false)
    expect(validateNewPassword('Abcdefgh1!')).toBe(true)
  })
})

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

:::информация Также опубликовано здесь< /а>.

:::


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE