Как провести unit-тестирование setcookie() в php?

Dmitriy Nyu
Dmitriy Nyu
Published in
4 min readJul 19, 2016

Однажды, я писал код для класса, который определял, какой рекламный попап показывать пользователю. Алгоритм следующий — проверяем, есть ли в браузере куки А, если такой куки нет — показываем попап, А и сохраняем куки А. Если куки, А уже установлена, проверяем сохранена ли кука Б, и либо показываем попап Б и сохраняем куку Б, либо идём проверять существование куки С и так далее. Не самое изящное, но простое решение.

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

Сегодня мне показалось, что это любопытный вопрос. Как лучше протестировать setcookie ()? Как определить юнит-тестом, что произошла установка куки? Это возможно?

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

Очевидная проблема — в режиме командной строки, который обычно используют для запуска тестов функции, связанные с заголовками ничего не делают, провести такую проверку невозможно. Но если у вас применяется веб-интерфейс, который запускает юнит-тесты через браузер, то это становится реальным. В этом случае, проверка тривиальна — создаем новый метод assertCookieSet ($params, $headers) внутри которого проверяем наличие в заголовках строчки, сформированный по спецификации, которая должна появиться после вызова setcookie ().

Правда, в этом случаем мы проверяем не факт установки куки, а то, что мы подготовили всё необходимое для браузера. Мы полагаемся на то, что браузер правильно поймёт наш заголовок и создаст куку. Получается, что в этом случае задача тоже не решена полностью — мы по-прежнему не знаем, использования нашего кода приводит к появлению куки у пользователя на компьютере или нет.

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

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

Стоит отметить ещё одну опасность setcookie (). Функция возвращает true даже если куки не могут быть установлены при вызовы через php-cli. False мы получаем либо в случае провала проверки на валидность некоторых параметров, либо в случае, если сделали вывод перед вызовом setcookie (). Вот определение функции на С:

PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, zend_string *path, zend_string *domain, int secure, int url_encode, int httponly)
{
char *cookie;
size_t len = sizeof("Set-Cookie: ");
zend_string *dt;
sapi_header_line ctr = {0};
int result;
zend_string *encoded_value = NULL;

if (!ZSTR_LEN(name)) {
zend_error( E_WARNING, "Cookie names must not be empty" );
return FAILURE;
} else if (strpbrk(ZSTR_VAL(name), "=,; \t\r\n\013\014") != NULL) { /* man isspace for \013 and \014 */
zend_error(E_WARNING, "Cookie names cannot contain any of the following '=,; \\t\\r\\n\\013\\014'" );
return FAILURE;
}

if (!url_encode && value &&
strpbrk(ZSTR_VAL(value), ",; \t\r\n\013\014") != NULL) { /* man isspace for \013 and \014 */
zend_error(E_WARNING, "Cookie values cannot contain any of the following ',; \\t\\r\\n\\013\\014'" );
return FAILURE;
}

len += ZSTR_LEN(name);
if (value) {
if (url_encode) {
encoded_value = php_url_encode(ZSTR_VAL(value), ZSTR_LEN(value));
len += ZSTR_LEN(encoded_value);
} else {
encoded_value = zend_string_copy(value);
len += ZSTR_LEN(encoded_value);
}
}

if (path) {
len += ZSTR_LEN(path);
}
if (domain) {
len += ZSTR_LEN(domain);
}

cookie = emalloc(len + 100);

if (value == NULL || ZSTR_LEN(value) == 0) {
/*
* MSIE doesn't delete a cookie when you set it to a null value
* so in order to force cookies to be deleted, even on MSIE, we
* pick an expiry date in the past
*/
dt = php_format_date("D, d-M-Y H:i:s T", sizeof("D, d-M-Y H:i:s T")-1, 1, 0);
snprintf(cookie, len + 100, "Set-Cookie: %s=deleted; expires=%s; Max-Age=0", ZSTR_VAL(name), ZSTR_VAL(dt));
zend_string_free(dt);
} else {
snprintf(cookie, len + 100, "Set-Cookie: %s=%s", ZSTR_VAL(name), value ? ZSTR_VAL(encoded_value) : "");
if (expires > 0) {
const char *p;
char tsdelta[13];
strlcat(cookie, COOKIE_EXPIRES, len + 100);
dt = php_format_date("D, d-M-Y H:i:s T", sizeof("D, d-M-Y H:i:s T")-1, expires, 0);
/* check to make sure that the year does not exceed 4 digits in length */
p = zend_memrchr(ZSTR_VAL(dt), '-', ZSTR_LEN(dt));
if (!p || *(p + 5) != ' ') {
zend_string_free(dt);
efree(cookie);
zend_string_release(encoded_value);
zend_error(E_WARNING, "Expiry date cannot have a year greater than 9999");
return FAILURE;
}
strlcat(cookie, ZSTR_VAL(dt), len + 100);
zend_string_free(dt);

snprintf(tsdelta, sizeof(tsdelta), ZEND_LONG_FMT, (zend_long) difftime(expires, time(NULL)));
strlcat(cookie, COOKIE_MAX_AGE, len + 100);
strlcat(cookie, tsdelta, len + 100);
}
}

if (encoded_value) {
zend_string_release(encoded_value);
}

if (path && ZSTR_LEN(path)) {
strlcat(cookie, COOKIE_PATH, len + 100);
strlcat(cookie, ZSTR_VAL(path), len + 100);
}
if (domain && ZSTR_LEN(domain)) {
strlcat(cookie, COOKIE_DOMAIN, len + 100);
strlcat(cookie, ZSTR_VAL(domain), len + 100);
}
if (secure) {
strlcat(cookie, COOKIE_SECURE, len + 100);
}
if (httponly) {
strlcat(cookie, COOKIE_HTTPONLY, len + 100);
}

ctr.line = cookie;
ctr.line_len = (uint)strlen(cookie);

result = sapi_header_op(SAPI_HEADER_ADD, &ctr);
efree(cookie);
return result;
}

Это означает, что если использовать возврат setcookie () без учёта контекста, у нас возникнет проблема и так делать нельзя:

public static function saveData()
{
return setcookie('key', 'value', strtotime('+1 month'));
}

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

Функция setcookie — http://php.net/manual/ru/function.setcookie.php
Стандарт — https://tools.ietf.org/html/rfc6265

--

--