Статьи

Ошибки: часть кривой обучения

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

Не думаю так? Посмотрите на эту ошибку Clojure:

Exception in thread "main" java.lang.ClassCastException:
 clojure.lang.PersistentList cannot be cast to clojure.lang.Symbol,
 compiling:(/home/giorgio/code/fizzbuzz-clojure/fizzbuzz.clj:9)

Это означало, что в моем коде была опечатка.

Мы можем сократить время отладки с помощью модульного тестирования для раннего обнаружения ошибок или путем разработки разобщенных компонентов для упрощения общей картины; даже с более строгими проверками типа, чтобы предотвратить некоторые ошибки проводки. Но когда появляется ошибка, самое время покопаться в ней и узнать больше о языке и платформе (или даже фреймворке), на которых вы стоите.

Я постараюсь перечислить некоторые языковые советы по устранению ошибок; поясняющий пример будет придерживаться PHP, поскольку это язык, ошибки которого я знаю лучше (это верно для каждого языка, с которым вы проводите много времени).

Обнаружение ошибок

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

Даже простая ошибка может быть пугающей, если вы не сразу знаете, что ее вызвало. «что» не может быть компонентом: это может быть контракт между двумя частями, составляющими ваше приложение. Возможно, дело не в том, что MySQL теряет записи (маловероятно), но пара запросов на вставку и чтение несовместимы друг с другом.

Чтобы отладить ошибки на синтаксическом или процедурном уровне, попробуйте итеративно:

  • запустить комплексный контрольный пример.
  • Убедитесь, что ошибок не возникло (тест может провалиться, поскольку ожидание не выполнено, но на этом шаге не должно быть зарегистрировано ни одной ошибки или исключения).
  • Добавьте строку или утверждение.

Когда вы получаете новую ошибку, вы нашли виновника. Давным-давно было принято писать 1000 строк кода перед его запуском; сейчас у нас итерации по 10 секунд для меня норма. Некоторые разработчики даже запускают модульные тесты постоянно, при каждой модификации исходного файла или каждые 2 секунды.

Рассмотрим это начальное состояние одного из ваших методов:

$result = 1;

После модификации это может выглядеть так:

$result = 1 + $array['field'];

If this change causes an error, either the + operator isn’t working or $array does not contain ‘field’ (or it can’t be accessed like this because it’s an object, or we are using the wrong syntax). Even a simple step like this can fail for 4 different reasons then. Try this instead:

$result = 1 + 4;

then

$result = $array['field'];

and then

$result = 1 + $array['field'];

and stop whenever there is trouble.

TDD does not always need to be so fine-grained that each line requires to rerun the current driving tests; the size of each step is a function of your confidence (in a direct correlation). The kind of small transformations you need to perform when you’re encountering a dubious error is similar to refactoring: in this case you’re changing the shape of the code to use different syntax, constructs or collaborations while keeping the same behavior.

The good thing is that with a good unit test suite running this code a thousand times has no overhead other than pushing a button.

Logging errors

Your (testing) framework should be able to gather every exception that is generated during a run, while compile-time errors will interrupt the test before it can be run. It should show you:

  • an error message.
  • The line that generated it.
  • The full stack trace.

If it doesn’t show you this, change framework. I wonder if this also applied to frameworks for production code…

Logging errors means that even if an exception bubbles up, it will be logged on a separate file by the interpreter or the virtual machine. This is interesting when running end-to-end tests as there are many layers that can swallow an exception between a fault and the endpoints where you are issuing commands and checking results.

Consider a user interface: its job is to hide technical errors to maintain a smooth user experience. So it’s normal to not be able to look at exceptions through the UI; nevertheless, logging can save you while running manual or automated end-to-end tests as everything that goes wrong is stored in a text file instead of just elling you «Oops! Come back later.»

For example, PHP’s log_errors directive tells the interpreter to write all errors to a separate file, without contaminating the output of PHP scripts with them; at the same time, this file will contain a list of what is going wrong at the language level (missing variables, undefined constants, or deprecated and dangerous features).

Understanding errors

Don’t assume that just because you are reading an error message you are able to debug it; understanding an error means first having the means to reproduce it, and then locating which class or method isn’t doing what you expect from it.

For example, the real source of an error can be far from where it is generated: just think of a dictionary with missing values. An error is generated when you access one of these values expecting it to be there, but the real problem lies in who has created the dictionary or modified it.

These actions at a distance can be minimized by our own paradigm (an object that wraps the array and intercepts all modifications) or principles (reducing the usage of global structures), but especially when you’re relying on the platform/framework/database they cannot be eliminated.

For example, I once traced an error happening at the end of a PHP process to code that was putting anonymous functions in the session. The session map was serialized at the end of the script and the non-serializable anonymous functions caused a cryptic error.

I think that by their nature these interaction errors cannot be avoided *if you don’t know they can happen* (once you are familiar with PHP and $_SESSION, you will naturally avoid to write this kind of code, like you would never try to serialize a database connection again).

Thus like the proverb says, Google and StackOverflow are our friends: I shall never ask for help without googling it first. It’s very likely that someone else has already encountered the same problem in another application and blogged about it, you’re encouraged to do your homework and be prepared when you’re explaining the error to one of your colleagues.

On the other hand, these interaction errors can be avoided if you know that that can happen: the trick is in discovering your false assumptions as quickly as possible and in a scenario where it’s cheap to fix them.

The integration tests proposed by GOOS are a way to minimize these external errors: instead of reading documentation, jump into writing automated tests that involve the external code (a database driver, for instance). These tests will verify that `SELECT COUNT(*) FROM user INNER JOIN group ON user.group_id = group.id` really returns the set of groups you want; or that db.users.find().sort({name:-1}) sorts in descending order. Discovering these facts through a user interface or as a bug in a browser would be inefficient and distracting.