Эта статья — продолжение истории про калькулятор на 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>
Посмотреть работу калькулятора на странице проекта.
Что дальше
Кажется, что теперь в самом коде есть что улучшить. В следующей серии займёмся рефакторингом. Не переключайтесь.
