Эта статья — продолжение истории про калькулятор на JavaScript. В предыдущих частях мы:
- написали простой калькулятор на JavaScript;
- отдали его UX-тестировщикам и обновили дизайн калькулятора по их замечаниям.
Теперь наш калькулятор будут смотреть инженеры по тестированию — постараются найти в нём ошибки логики, поведения, вычислений и прочие неявные моменты.
👉 В этой статье не будет автотестов, юнит-тестов, API-тестов и интеграционных тестов. Это всё нужные инструменты, но задача тестировщика, в частности, — выбрать правильный инструмент тестирования.
Если у него в работе очень простая программа (как наш калькулятор), то нет смысла городить автоматизацию и делать тесты ради тестов. Иногда можно и вручную всё проверить и получить точно такой же результат.
Базовое тестирование
Первое, что мы делаем, — проверяем, а как вообще ведут себя кнопки математических действий и делают ли они то, что нужно. Возьмём два числа — 12 и 5 — и сравним результаты всех действий с тем, что даёт калькулятор:
12 + 5 = 17
12 − 5 = 7
12 × 5 = 60
12 / 5 = 2,4
Отрицательные числа тоже отображаются и считаются правильно.
Тестируем большие числа
У компьютеров есть нюанс: любые переменные имеют ограничения по размеру числа. Например, если на переменную выделено 16 бит, то максимальное число, которое можно в нее положить, — 65 536. Число на единицу больше уже потребует 17 бит, а мы столько не выделяли.
Мы хоть и разработчики этого калькулятора, но мы не помним, какое число имели в виду, когда заводили переменную. Может быть, это решение за нас принял JavaScript. Поэтому нужно проверить, не сломается ли наш калькулятор от больших чисел.
Пробуем: 123 456 789 × 2 = 246 913 578 — верно
А вот необычный эксперимент:
12 345 678 901 234 567 × 1 = 12 345 678 901 234 568
Ух ты! Мы умножили большое число на единицу, а в ответе появилась ошибка. Это значит, что настолько длинные числа за раз наш калькулятор уже обработать не в состоянии.
Записываем баг:
❌ Неправильно обрабатываются 17-значные числа и те, которые больше них.
А если мы попробуем получить 17-значное число в ответе, интересно, оно тоже будет с ошибкой?
Да, в ответе тоже неверное число — 8 × 4 = 32, поэтому в конце должно стоять 2, а не 0. Пишем баг:
❌ Если в ответе получается 17-значное число или более — ответ точно неверный.
При этом деление на 16-значное число работает верно:
Тестирование математических трюков
Теперь попробуем разделить на ноль:
Скрипт хитро выкрутился и записал результатом деления бесконечность. Но лучше выводить сообщение, что на ноль делить нельзя.
❌ Нет сообщения при делении на ноль.
Отказоустойчивость
А что если оставить поле ввода пустым и попробовать что-то посчитать? Давайте посмотрим:
Скрипт преобразовал пустую строку в ноль и получил ответ, но это неправильно — при отсутствии одного из чисел калькулятор должен сообщить об этом, а не продолжать считать.
❌ Нет сообщения, если одно из чисел не введено.
Пойдём дальше и введём слово вместо числа:
Скрипт честно пытается перевести строку в число, у него это не получается, поэтому он выдаёт неопределённое значение.
❌ Нет проверки на то, ввели число или строку.
И напоследок проверим что будет, если мы что-то введём, но не выберем ни одно действие:
Тоже плохо. Надо будет обработать такую ситуацию.
❌ Нет проверки, когда не выбрали ничего из математических действий.
Так проверяем работу калькулятора со всеми действиями, а не только с умножением. В итоге у нас получится список ошибок, которые нужно исправить.
Что делаем
После тестирования у нас получился такой список ошибок:
❌ Неправильно обрабатываются 17-значные числа и те, которые больше них.
❌ Если в ответе получается 17-значное число или более — ответ точно неверный.
❌ Нет сообщения при делении на ноль.
❌ Нет сообщения, если одно из чисел не введено.
❌ Нет проверки на то, ввели число или строку.
❌ Нет проверки, когда не выбрали ничего из математических действий.
Исправим эти ошибки. Так как все вычисления начинаются при вызове функции func(), то и править всё будем тоже внутри неё.
Проверяем, что ввели число, а не слова или другие символы
Для перевода строку в число в JavaScript используют функцию Number()
. Если строку можно перевести в число — она сработает без ошибок, а если не получится — вернёт значение NaN. Это значит, что числа не получилось.Чтобы проверить, прошло всё нормально или нет, мы будем использовать функцию isNaN()
— она сравнит значение переменной с NaN, и вернёт true, если в переменной лежит NaN. А чтобы не путать числа и строки для сравнения, переименуем переменные в самом начале скрипта и сделаем их принудительно строками:
// получаем первое и второе число
var num1_str = String(document.getElementById("num1").value);
var num2_str = String(document.getElementById("num2").value);
// переводим строки в числа
let num1 = Number(num1_str)
let num2 = Number(num2_str)
// проверяем, получилось ли число из первой строки или нет
if (isNaN(num1)) {
// если не получилось — пишем сообщение
document.getElementById("result").innerHTML = 'Калькулятор не может распознать первое число. Проверьте его, пожалуйста';
// и выходим из функции
return;
}
// проверяем, получилось ли число из второй строки или нет
if (isNaN(num2)) {
// если не получилось — пишем сообщение
document.getElementById("result").innerHTML = 'Калькулятор не может распознать второе число. Проверьте его, пожалуйста';
// и выходим из функции
return;
}
Проверяем, что нет пустых значений
JavaScript когда переводит строку в число, то пустую строку он считает как 0. Нам такой вариант не подходит, поэтому сравним её с пустой сторокой. Если она пустая — выдаём сообщение и ничего не считаем.
Ещё надо дополнительно добавить проверку на пробелы — JavaScript строку из пробелов тоже переводит как ноль, а нам это не нужно:
// получаем первое и второе число
var num1_str = String(document.getElementById("num1").value);
var num2_str = String(document.getElementById("num2").value);
// проверяем, не пустая ли первая строка
if ((num1_str.length == 0) || (num1_str.indexOf(' ') != -1)) {
// если пустая — пишем сообщение
document.getElementById("result").innerHTML = 'Вы не ввели первое число или добавили пробел в поле ввода';
// и выходим из функции
return;
}
// проверяем, не пустая ли вторая строка
if ((num2_str.length == 0) || (num2_str.indexOf(' ') != -1)) {
// если пустая — пишем сообщение
document.getElementById("result").innerHTML = 'Вы не ввели второе число или добавили пробел в поле ввода';
// и выходим из функции
return;
}
Обрабатываем деление на ноль
Простая проверка — добавляем сравнение второго числа с нулём:
// проверяем второе число при делении
if ((num2 == 0) && (op == '/')) {
// если не получилось — пишем сообщение
document.getElementById("result").innerHTML = 'На ноль делить нельзя';
// и выходим из функции
return;
}
Обрабатываем длинные числа
Даже если мы ограничим каждое поле ввода числами по 16 знаков вместо 17, то при перемножении они дадут нам в ответе 32 знака — а это тоже превышает наш предел точности. Чтобы гарантированно получить в ответе число не больше 16 разрядов перед запятой, нам нужно, чтобы оба числа были не больше 99 999 999 — в нём 8 разрядов, а при перемножении мы получим максимум 16, как раз то, что нужно.
Чтобы это сделать, добавим проверку на размер числа:
// проверяем размер чисел
if ((num1 > 99999999) || (num2 > 99999999)) {
// если не помещается одно из них в диапазон — пишем сообщение
document.getElementById("result").innerHTML = 'Калькулятор может работать с числами не больше 99 999 999';
// и выходим из функции
return;
}
Если не выбрано математическое действие
С этим всё просто — добавляем в case действие по умолчанию, которое выполнится, если никакие из вариантов не подойдут:
// смотрим, что было в переменной с действием, и действуем исходя из этого
switch (op) {
case '+':
result = num1 + num2;
break;
case '-':
result = num1 - num2;
break;
case '*':
result = num1 * num2;
break;
case '/':
result = num1 / num2;
break;
default: result = 'Выберите действие'
}
В итоге
✅ Калькулятор не работает с числами больше 16 знаков до запятой и предупреждает об этом пользователя
✅ В ответе всегда число, в котором не больше 16 знаков до запятой
✅ Есть проверка деления на ноль
✅ Есть сообщение, если одно из чисел не введено.
✅ Есть проверка на то, ввели число или строку.
✅ Есть проверка, когда не выбрали ничего из математических действий.
Это всё?
О нет, этот калькулятор можно гонять ещё и в хвост и в гриву:
- Протестировать десятичные дроби и операции с ними.
- Вставлять в поля ввода изображения и файлы.
- Устраивать переполнение буфера браузера.
- Совершать 10 миллионов вычислений в секунду.
- Запускать одновременно 10 миллионов калькуляторов.
- Запустить калькулятор в 1911 году.
- Засунуть в него комплексные числа.
- Засунуть в него самое большое простое число (и разделить).
- Засунуть в него кота.
Это (и многое другое) — и есть работа тестировщика. Круто, да?
Приходите учиться на тестировщиков
в «Практикум» →
И ни одна кошка не пострадает.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Размеры шрифтов</title>
<style type="text/css">
/*задаём общие параметры для всей страницы: шрифт и отступы*/
body {
text-align: center;
margin: 10;
font-family: Verdana, Arial, sans-serif;
font-size: 16px;
}
/* настраиваем внешний вид полей ввода*/
input {
display: inline-block;
margin: 20px auto;
border: 2px solid #eee;
padding: 10px 20px;
font-family: Verdana, Arial, sans-serif;
font-size: 16px;
}
/* внешний вид кнопок */
button{
font-family: Verdana, Arial, sans-serif;
font-size: 16px;
margin: 10px;
padding: 10px;
}
/* стиль подсветки выбранной операции */
.light{
background-color: yellow;
}
</style>
</head>
<body>
<!-- заголовок -->
<h1>Калькулятор</h1>
<!-- поле ввода первого числа -->
<input id="num1" />
<!-- блок с кнопками -->
<div id="operator_btns">
<button id="plus" onclick="sel_ligth('plus')">+</button>
<button id="minus" onclick="sel_ligth('minus')">-</button>
<button id="times" onclick="sel_ligth('times')">x</button>
<button id="divide" onclick="sel_ligth('divide')">:</button>
</div>
<!-- поле ввода второго числа -->
<input id="num2" />
<br>
<!-- кнопка для расчётов -->
<button onclick="func()">Посчитать</button>
<!-- здесь будет результат -->
<p id="result"></p>
<!-- наш скрипт -->
<script>
// переменная, в которой хранится выбранное математическое действие
var op;
// функция, которая подсветит выбранное математическое действие
function sel_ligth(sel_id) {
// убираем класс подсветки со всех кнопок
document.getElementById("plus").classList.remove("light");
document.getElementById("minus").classList.remove("light");
document.getElementById("times").classList.remove("light");
document.getElementById("divide").classList.remove("light");
// и добавляем его только к нажатой
document.getElementById(sel_id).classList.add("light");
// в зависимости от нажатой клавиши меняем значение переменной op
switch (sel_id) {
case "plus":
op = "+"
break;
case 'minus':
op = '-'
break;
case 'times':
op = "*"
break;
case 'divide':
op = "/"
break;
}
}
// добавляем обработчик нажатия на клавиши ко второму полю ввода
document.getElementById("num2").addEventListener('keydown', function(e) {
if (e.keyCode === 13) {
func();
}
});
// функция расчёта
function func() {
// переменная для результата
var result;
// получаем первое и второе число
var num1_str = String(document.getElementById("num1").value);
var num2_str = String(document.getElementById("num2").value);
// проверяем, не пустая ли первая строка
if ((num1_str.length == 0) || (num1_str.indexOf(' ') != -1)) {
// если пустая — пишем сообщение
document.getElementById("result").innerHTML = 'Вы не ввели первое число или добавили пробел в поле ввода';
// и выходим из функции
return;
}
// проверяем, не пустая ли вторая строка
if ((num2_str.length == 0) || (num2_str.indexOf(' ') != -1)) {
// если пустая — пишем сообщение
document.getElementById("result").innerHTML = 'Вы не ввели второе число или добавили пробел в поле ввода';
// и выходим из функции
return;
}
// переводим строки в числа
let num1 = Number(num1_str)
let num2 = Number(num2_str)
// проверяем, получилось ли число из первой строки или нет
if (isNaN(num1)) {
// если не получилось — пишем сообщение
document.getElementById("result").innerHTML = 'Калькулятор не может распознать первое число. Проверьте его, пожалуйста';
// и выходим из функции
return;
}
// проверяем, получилось ли число из второй строки или нет
if (isNaN(num2)) {
// если не получилось — пишем сообщение
document.getElementById("result").innerHTML = 'Калькулятор не может распознать второе число. Проверьте его, пожалуйста';
// и выходим из функции
return;
}
// проверяем размер чисел
if ((num1 > 99999999) || (num2 > 99999999)) {
// если не помещается одно из них в диапазон — пишем сообщение
document.getElementById("result").innerHTML = 'Калькулятор может работать с числами не больше 99 999 999';
// и выходим из функции
return;
}
// проверяем второе число при делении
if ((num2 == 0) && (op == '/')) {
// если не получилось — пишем сообщение
document.getElementById("result").innerHTML = 'На ноль делить нельзя';
// и выходим из функции
return;
}
// смотрим, что было в переменной с действием, и действуем исходя из этого
switch (op) {
case '+':
result = num1 + num2;
break;
case '-':
result = num1 - num2;
break;
case '*':
result = num1 * num2;
break;
case '/':
result = num1 / num2;
break;
default: result = 'Выберите действие'
}
// отправляем результат на страницу
document.getElementById("result").innerHTML = result;
}
</script>
</body>
</html>
Посмотреть работу калькулятора на странице проекта.
Что дальше
Кажется, что теперь в самом коде есть что улучшить. В следующей серии займёмся рефакторингом. Не переключайтесь.