1. Code
  2. PHP

Pruebas expresivas con Hamcrest

Hamcrest es un conjunto de comparadores para escribir código más expresivo. Da la casualidad de que estos comparadores son especialmente útiles al escribir pruebas. En este artículo, veremos Hamcrest para PHP.
Scroll to top
11 min read

Spanish (Español) translation by Steven (you can also view the original English article)

Hamcrest es un conjunto de comparadores para escribir código más expresivo. Da la casualidad de que estos comparadores son especialmente útiles al escribir pruebas. En este artículo, veremos Hamcrest para PHP.


¿Qué es Hamcrest?

Cada comparador en Hamcrest te ayuda a escribir pruebas que se leen de forma muy natural.

La expresividad de Hamcret se originó con JMock, pero no fue hasta la adición del método único assertThat() que fue refactorizado en una biblioteca autónoma y utilizable de forma independiente en frameworks de pruebas.

Después de su adopción inicial de Java, las implementaciones de Hamcrest en varios lenguajes de programación estuvieron disponibles. JUnit, RSpec y otros frameworks de pruebas implementan la sintaxis nativa de Hamcrest, eliminando la necesidad de incluir explícitamente cualquier biblioteca. Debido a la rápida adopción de Hamcret, los frameworks de pruebas se categorizaron de la siguiente manera:

  • Los frameworks de prueba de primera generación eran muy básicos, ya que tenían un único método assert() con un uso como: assert(x==y). Los programadores tenían dificultades para redactar pruebas expresivas y bien organizadas. También requería conocimientos de programación para comprender condiciones más complejas y dificultaba la escritura.
  • Los frameworks de prueba de segunda generación, como PHPUnit, ofrecen un gran conjunto de afirmaciones diferentes. Estos frameworks extrajeron la acción o el predicado de los parámetros (x == y) en los nombres de las aserciones, como: assertEquals($expected, $actual). Esto hizo que las pruebas fueran más expresivas y facilitó la definición de funciones de aserción personalizadas.
  • Los frameworks de prueba de tercera generación utilizan un único método de aserción (assertThat()) junto con comparadores expresivos, lo que hace que las aserciones se lean como oraciones en inglés: assertThat($calculatedResult, equalTo($expectedResult)) en contraste con assertEquals($expectedResult, $calculatedResult).

El uso de comparadores en Hamcrest también puede ayudar de otras formas; puedes escribir tus comparadores personalizados y usarlos dentro de la función assertThat(). Hamcret también proporciona mucha más información cuando algo sale mal. En lugar de un mensaje oscuro como "El valor esperado no es verdadero", los errores de Hamcrest en realidad indican todos los valores involucrados en la prueba, es decir, tanto los valores esperados como los reales. Los comparadores también permiten afirmaciones flexibles, por lo que las pruebas no fallan después de realizar pequeñas modificaciones que no deberían romper la prueba. En otras palabras, las pruebas frágiles son más sólidas.


Instalando Hamcrest para PHP

Hay varias formas de instalar Hamcrest. Los dos más comunes implican usar PEAR o descargar el código fuente. En el momento de escribir este artículo, Hamcrest para PHP aún no está disponible a través de Composer.

Usar PEAR

Usar PEAR para instalar Hamcrest es fácil. Simplemente ejecuta los siguientes comandos:

1
pear channel-discover hamcrest.googlecode.com/svn/pear
2
pear install hamcrest/Hamcrest

Asegúrate de verificar que tienes la carpeta de instalación de PEAR en tu ruta global. Esto facilita la inclusión de Hamcrest en tus pruebas.

Descargar archivo de origen

Siempre puedes descargar la última versión de Hamcrest desde la página de descarga del proyecto y usarla como cualquier biblioteca PHP de terceros.


Nuestra primera prueba

Primero asegurémonos de tener una prueba de esqueleto en funcionamiento con Hamcrest habilitado. Crea un proyecto en tu IDE o editor de código favorito y crea un archivo de prueba. Acabo de crear un nuevo proyecto en NetBeans con una carpeta llamada Test como carpeta principal. Dentro de esta carpeta hay un archivo vacío llamado HamcrestMatchersForPHPTest.php. Este será el archivo de prueba, y su contenido es el siguiente:

1
require_once 'Hamcrest/Hamcrest.php';
2
3
class HamcrestMatchersForPHPTest extends PHPUnit_Framework_TestCase {
4
5
	function testHamcrestWorks() {
6
		assertThat('a', is(equalTo('a')));
7
	}
8
}

La primera línea incluye la biblioteca Hamcrest. Ten en cuenta que es con "H" mayúscula tanto para la carpeta como para el nombre del archivo. El archivo 'léame' oficial de Hamcrest contiene un error tipográfico.

A continuación, nuestra única prueba, testHamcrestWorks(), afirma que a es igual a a. Obviamente, esta es una prueba de aprobación.

Passing TestPassing TestPassing Test

Este primer ejemplo simple es natural de leer. Afirma que 'a' es igual a 'a'. Casi no necesita explicación.

El único método de aserción que usaremos en nuestras pruebas es assertThat(). El comparador is() es simplemente endulzante sintáctico; no hace nada más que construir tu oración. Finalmente, el igualador equalTo() compara el primer parámetro de assertThat() con el valor proporcionado a equalTo(). Esta prueba se traduce efectivamente como 'a' == 'a'.


Comparación de números

Comencemos con un ejemplo simple:

1
function testNumbers() {
2
	assertThat(2, equalTo(2));
3
4
	assertThat(2, lessThan(3));
5
	assertThat(3, greaterThan(2));
6
7
	assertThat(2, is(identicalTo(2)));
8
	assertThat(2, comparesEqualTo(2));
9
10
	assertThat(2, is(closeTo(3, 1)));
11
12
	assertThat(2, is(not(3)));
13
}

Puedes escribir tus comparadores personalizados y usarlos dentro de la función assertThat().

Este código presenta nuevos comparadores; los dos primeros son lessThan() y greaterThan(). Estos comparadores son equivalentes a los operadores de comparación "<" y ">".

El método identicalTo() proporciona otro medio de comprobar la igualdad. De hecho, este es el equivalente del operador ===, mientras que equalTo() es ==. Otro nuevo comparador en este código es comparesEqualTo(), que realiza efectivamente el equivalente de !($x $y). En el código anterior, tanto $x como $y son el valor 2 (por lo que pasa la prueba).

Uno de los comparadores más interesantes es closeTo(). Acepta dos argumentos: el valor objetivo y la diferencia permitida, respectivamente. Este ejemplo comprueba si 2 está cerca de 3 en un máximo de 1. Esto es obviamente correcto y la afirmación pasa.

Finalmente, la última afirmación es solo una simple negación combinada con is(). Obviamente, esto afirma que 2 no es 3.

Coincidencias de números combinados

Hamcrest también proporciona comparadores que equivalen a los operadores de comparación <= y >=. Se llaman acertadamente lessThanOrEqualTo() y greaterThanOrEqualTo(), y se usan así:

1
function testNumbersComposed() {
2
	assertThat(2, lessThanOrEqualTo(2));
3
	assertThat(2, lessThanOrEqualTo(3));
4
5
	assertThat(3, greaterThanOrEqualTo(3));
6
	assertThat(3, greaterThanOrEqualTo(2));
7
8
	assertThat(2, is(atMost(2)));
9
	assertThat(2, is(atMost(3)));
10
11
	assertThat(3, is(atLeast(3)));
12
	assertThat(3, is(atLeast(2)));
13
}

Hamcrest también proporciona los comparadores atMost() y atLeast(). Los comparadores lessThanOrEqualTo() y atMost() son idénticos. Ambos equivalen a $x. Naturalmente, greaterThanOrEqualTo() y atLeast() realizan exactamente lo contrario, comprobando que $x >= $y.


Trabajando con cadenas

Hamcrest también proporciona varios comparadores para trabajar con cadenas de caracteres. Aquí hay unos ejemplos:

1
function testStrings() {
2
	assertThat('this string', equalTo('this string'));
3
	assertThat('this string', equalToIgnoringCase('ThiS StrIng'));
4
	assertThat('this string', equalToIgnoringWhiteSpace('   this   string   '));
5
	//assertThat('this string', equalToIgnoringWhiteSpace('thisstring'));

6
	assertThat('this string', identicalTo('this string'));
7
8
	assertThat('this string', startsWith('th'));
9
	assertThat('this string', endsWith('ing'));
10
11
	assertThat('this string', containsString('str'));
12
	assertThat('this string', containsStringIgnoringCase('StR'));
13
14
	assertThat('this string', matchesPattern('/^this\s*/'));
15
}

Te recomiendo que uses los comparadores más expresivos siempre que sea posible...

Obviamente, los comparadores equalTo() e identicalTo() funcionan con cadenas y se comportan exactamente como cabría esperar. Pero como puedes ver, Hamcrest proporciona otros igualadores de comparación que serán específicos para las cadenas de caracteres. Como sus nombres implican, los comparadores equalToIgnoringCase() y equalToIgnoringWhiteSpace() coinciden con las cadenas ignorando el caso y los espacios en blanco, respectivamente.

Otros comparadores, como startsWith() y endsWith(), comprueban si la subcadena especificada está al principio o al final de la cadena real. El comparador containsString() comprueba si la cadena contiene la subcadena proporcionada. El comparador containsString() también se puede extender con containsStringIgnoringCase(), agregando insensibilidad a mayúsculas y minúsculas.

El comparador matchesPattern() incorpora expresiones regulares para encontrar una coincidencia en la cadena. Puedes usar cualquier expresión regular y, en muchos casos, esta solución es necesaria en cadenas más complejas. En cualquier caso, te recomiendo que uses los comparadores más expresivos siempre que sea posible y solo recurras a expresiones regulares si es absolutamente necesario; hacerlo hace que tu código sea más legible para todos.

Coincidencia de cadenas vacías

Es común probar si una cadena está vacía. Hamcrest te respalda con eso.

1
function testStringEmptiness() {
2
	assertThat('', isEmptyString());
3
	assertThat('', emptyString());
4
	assertThat('', isEmptyOrNullString());
5
	assertThat(NULL, isEmptyOrNullString());
6
	assertThat('', nullOrEmptyString());
7
	assertThat(NULL, nullOrEmptyString());
8
9
	assertThat('this string', isNonEmptyString());
10
	assertThat('this string', nonEmptyString());
11
}

Sí, hay muchos comparadores para verificar si una cadena está vacía. Cada variante tiene una versión con "is" por delante. Esencialmente, emptyString() e isEmptyString() son idénticos, y lo mismo es cierto para los otros comparadores. Los comparadores nonEmptyString() e isNonEmptyString() también se pueden escribir así:

1
assertThat('this string', not(isEmptyString()));
2
assertThat('this string', is(not(emptyString())));
3
assertThat('this string', is(nonEmptyString()));

Pero, por supuesto, esas variantes pueden agregar más verbosidad, lo que dificulta la comprensión del código a primera vista.


Inclusiones y exclusiones

Aquí hay algunos buenos enfoques para determinar si una variable pertenece o no a un grupo:

1
function testInclusionsExclusions() {
2
	assertThat('val', is(anyOf('some', 'list', 'of', 'val')));
3
	assertThat('val', is(noneOf('without', 'the', 'actual', 'value')));
4
5
	assertThat('this string', both(containsString('this'))->andAlso(containsString('string')));
6
	assertThat('this string', either(containsString('this'))->orElse(containsString('that')));
7
8
	assertThat('any value, string or object', is(anything()));
9
}

Estos ejemplos usan cadenas, pero puedes usar estos comparadores con variables como objetos, matrices, números, etc. Los comparadores anyOf() y noneOf() determinan si la variable esperada reside o no en la lista de valores proporcionada.

Los otros dos comparadores, both() y either(), se utilizan comúnmente con los modificadores andAlso() y orElse(). Estos son equivalentes de:

1
assertThat((strpos('this string', 'this') !== FALSE) && (strpos('this string', 'string') !== FALSE));
2
assertThat((strpos('this string', 'this') !== FALSE) || (strpos('this string', 'string') !== FALSE));

Finalmente, anything() coincide... bueno, cualquier cosa. Tiene un parámetro de cadena opcional para fines de metadatos, lo que ayuda a cualquiera que lea la prueba a comprender mejor por qué una afirmación siempre debe coincidir.


Matrices

Los comparadores de matrices son probablemente los comparadores más complejos y útiles proporcionados por Hamcrest. El código de esta sección proporciona una lista de trucos para hacer que las afirmaciones complicadas basadas en matrices se conviertan en un código que se lee como una prosa bien escrita.

Ten en cuenta: No hago ninguna diferencia entre matrices y hashes en estos ejemplos. Solo hablo de matrices, pero todo se aplica también a los hash.

Igualdad de matrices

1
function testArrayEquality() {
2
	$actualArray = array(1,2,3);
3
4
	$expectedArray = $actualArray;
5
	assertThat($actualArray, is(anArray($expectedArray)));
6
	assertThat($actualArray, equalTo($expectedArray));
7
}

Estos métodos destacan diferentes formas de comparar la igualdad de dos matrices. La primera versión usa el comparador anArray() con un nombre engañoso. En realidad, compara las dos matrices elemento por elemento. En caso de falla, solo el primer conjunto de elementos desiguales se muestra en el mensaje de error.

La segunda versión, que usa equalTo(), también compara cada elemento en las matrices, pero genera ambas matrices en su totalidad en caso de falla. Naturalmente, la longitud de tus matrices determinará qué comparador utilizarás. La lectura de matrices grandes puede resultar difícil.

Coincidencias parciales de matrices

En muchos casos, simplemente queremos verificar si una matriz contiene ciertos elementos. Y Hamcrest nos respalda con eso.

1
function testArrayPartials() {
2
	$actualArray = array(1,2,3);
3
4
	assertThat($actualArray, hasItemInArray(2));
5
	assertThat($actualArray, hasValue(2));
6
7
	assertThat($actualArray, arrayContaining(atLeast(0),2,3));
8
	assertThat($actualArray, contains(1,2,lessThan(4)));
9
10
	assertThat($actualArray, arrayContainingInAnyOrder(2,3,1));
11
	assertThat($actualArray, containsInAnyOrder(3,1,2));
12
}

Los comparadores hasItemInArray() y hasValue() son idénticos; ambos comprueban si el valor proporcionado o el resultado del comparador existe en la matriz. Proporcionar un valor como argumento es equivalente a usar equalTo(). Por lo tanto, estos son idénticos: hasValue(2) y hasValue(equalTo (2)).

Los siguientes dos comparadores, arrayContaining() y contains(), también son idénticos. Verifican, en orden, que cada elemento de la matriz satisfaga al comparador, o que cada elemento sea igual a los valores especificados.

Finalmente, como puedes deducir fácilmente del ejemplo anterior, arrayContainingInAnyOrder() y containsInAnyOrder() son los mismos que los dos comparadores anteriores. La única diferencia es que no les importa el orden.

Claves de matriz que coincidan

1
function testArrayKeys() {
2
	$actualArray['one'] = 1;
3
	$actualArray['two'] = 2;
4
	$actualArray['three'] = 3;
5
6
	assertThat($actualArray, hasKeyInArray('two'));
7
	assertThat($actualArray, hasKey('two'));
8
9
	assertThat($actualArray, hasKeyValuePair('three', 3));
10
	assertThat($actualArray, hasEntry('one', 1));
11
}

Los comparadores hasKeyInArray() y hasKey() verifican si el argumento dado coincide con alguna clave en la matriz, mientras que los dos últimos comparadores devuelven verdadero solo si se encuentran la clave y el valor. Entonces, un comparador como hasEntry('one', 2); habría fallado nuestra prueba porque en nuestra matriz, en la clave 'one' tenemos el valor 1 y no 2.

Ten en cuenta: Se recomienda utilizar la versión más corta (hasKey) cuando sea obvio por el nombre de la variable que es una matriz. Quien lea tu código puede estar confundido acerca del tipo de variable, en cuyo caso, (hasKeyInArray) puede ser más útil.

Tamaños de matriz

Puedes comprobar fácilmente el tamaño de una matriz con tres comparadores:

1
function testArraySizes() {
2
	$actualArray = array(1,2,3);
3
4
	assertThat($actualArray, arrayWithSize(3));
5
	assertThat($actualArray, nonEmptyArray());
6
	assertThat(array(), emptyArray());
7
}

El comparador arrayWithSize() comprueba un tamaño específico, nonEmtpyArray() comprueba si la matriz tiene al menos un elemento y emptyArray() verifica si la matriz dada está vacía.

Ten en cuenta: Existen versiones de estos tres comparadores si prefieres objetos iterables. Simplemente reemplaza "Array" con "Traversable" como traversableWithSize().


Comprobación de tipos de valor

Estos son los últimos comparadores. Prácticamente puedes verificar cualquier tipo de dato en PHP.

1
function testTypeChecks() {
2
	assertThat(NULL, is(nullValue()));
3
	assertThat('', notNullValue());
4
	assertThat(TRUE, is(booleanValue()));
5
	assertThat(123.45, is(numericValue()));
6
	assertThat(123, is(integerValue()));
7
	assertThat(123.45, is(floatValue()));
8
	assertThat('aString', stringValue());
9
	assertThat(array(1,2,3), arrayValue());
10
	assertThat(new MyClass, objectValue());
11
	assertThat(new MyClass, is(anInstanceOf('MyClass')));
12
	// there are a few other more exotic value checkers you can discover on your own

13
}

Estos se explican por sí mismos. Simplemente proporciona un valor u objeto y usa uno de los comparadores fáciles de usar para determinar tu tipo de dato.


Conclusiones

Hamcrest PHP no está ampliamente documentado en la documentación oficial, y espero que este tutorial ayude a explicar los comparadores que proporciona. Hay algunos comparadores que no se enumeran en este tutorial, pero son muy exóticos y rara vez se utilizan.

Cada comparador de Hamcrest te ayuda a escribir pruebas que se leen con mucha naturalidad.

Si no son suficientes para ti, puedes agregar tus propios comparadores para cualquier situación en la que los necesites. Espero que hayas disfrutado de este tutorial y gracias por leer.