Buenas prácticas de testing en Etsy Parte 1

Este artículo es un un conjunto de buenas prácticas para testing utilizadas en Etsy. Es la primera parte de una traducción de este documento sobre buenas prácticas de testing.

¿Qué es este documento?

Esto es una introducción a las ideas y aproximaciones que motivan el buen testing. Tomaremos un ejemplo de un sistema escrito en PHP y testeado con PHPUnit y discutiremos como testear un buen diseño y como diseñar teniendo en mente la testesabilidad.

etsy logo

Audiencia y prerrequesitos

Este documento está pensado para cualquier ingeniero de Etsy.

Antes de esta guía, tu podías hacer cualquier cambio en el código de Etsy, preferible pero no necesariamente en PHP, incluyendo test unitarios.

¿Por qué test?

Aquí unas pocas razones.
corrección Estar seguro, en mayor o menor medida, que el código que has escrito no tiene bugs y funciona de manera “correcta”.
Estabilidad Estar seguro, en mayor o menor medida, que cambios en otras partes del código no romperán tu código.
Diseño un test es una prueba de concepto que enseña tu trabajo y tu esfuerzo en el código. ¿Como de entendible/legible es el API? ¿Cómo de sencillo es añadir un cambio al código ? Si los tests son difíciles de escribir, seguro que tu código es difícil de usar.

Piensa en cajas con entrada y salida

La idea básica de un test es proveer a tu código de una entrada, hacer algo con esas entradas y entonces chequear que la salida es la que esperabas.

Para escribir esta guía nosotros vamos a poner como ejemplo un sistema como el siguiente: un cola de trabajos, donde tu envías un “trozo de trabajo”(añades un elemento a la cola) , este se ejecuta en un “servidor central” y ese trabajo sale de alguna manera. Las comillas están puestas de forma deliberada. Los detalles técnicos sobre los “trozos de trabajo” y sobre el “servidor central” son irrelevantes. Lo importante es quedarse con la idea principal es intentar abstraer tanto en los test como en el código todo lo irrelevante.

Aquí tenemos la clase __JobServer_ que sólo tiene un método público llamado send.

<!--?php class JobServer { /** @var array a set of jobs we've already run recently and don't want to run again for now */ private $recentlyRunJobs = array(); /** * Attempts to run a given Job and returns a result code. * * @param Job $job the Job to run * @return int a code for the result of attempting to run $job: * JobResult::SUCCESS if the job was successfully run, * JobResult::FAILED_DURING_RUN if the job threw an exception, * JobResult::FAILED_TO_SCHEDULE if this job has already been run recently */ public function runJob(Job $job) { if (in_array($job, $this->recentlyRunJobs)) {
return JobResult::FAILED_TO_SCHEDULE;
}
try {
$job->run();
} catch (Exception $jobException) {
Logger::log_info("Exception during execution: " . $jobException->getMessage());
return JobResult::FAILED_DURING_RUN;
}
$this->recentlyRunJobs[] = $job;
return JobResult::SUCCESS;
}
}

Así qué, ¿ qué vamos a testear? Tenemos qué pensar en términos de API y en la clase como un contrato:
– Si se envía a la cola un trabajo que no se ha ejecutado recientemente , este se ejecuta y devuelve ‘JobResult::SUCCESS’
– Si se envía un trabajo que hace saltar una excepción durante su ejecución. Este se ejecutará y al finalizar se devuelve ‘JobResult::FAILED_DURING_RUN’
– Si se envía un trabajo que se ha ejecutado recientemente. Se devuelve ‘JobResult::FAILED_TO_SCHEDULE

Así que en nuestros tests unitarios debemos testear cada punto del contrato/API.
Ponemos los tests dentro de la ruta test/phpunit/JobServerTest.php y poner nombres a los métodos que indiquen uno de los aspectos de runJob que se esta probando cada vez.

<br />class JobServerTest extends PHPUnit_Framework_TestCase{

public function testRunJob_success() {
$jobServer = new JobServer();
$job = new SuccessfulTestJob();

$jobResult = $jobServer->runJob($job);
$this->assertEquals(JobResult::SUCCESS, $jobResult);
$this->assertTrue($job->jobRan);
}

public function testRunJob_failsDuringRun() {
$jobServer = new JobServer();
$job = new FailingTestJob();

$jobResult = $jobServer->runJob($job);
$this->assertTrue($job->jobRan);
$this->assertEquals(JobResult::FAILED_DURING_RUN, $jobResult);
}

public function testRunJob_failsToSchedule() {
$jobServer = new JobServer();
$job = new SuccessfulTestJob();

$jobResult = $jobServer->runJob($job);
$this->assertEquals(JobResult::SUCCESS, $jobResult);
$jobResult = $jobServer->runJob($job);
$this->assertEquals(JobResult::FAILED_TO_SCHEDULE, $jobResult);
}
}

/* A test-specific implementation like this is called a "stub". */
class SuccessfulTestJob implements Job {

public $jobRan = false;

public function run() {
$this->jobRan = true;
}
}

class FailingTestJob implements Job {

public $jobRan = false;

public function run() {
$this->jobRan = true;
throw new RuntimeException("Oopsie");
}
}

Cada test responde una pregunta muy importante ¿Qué estoy probando exactamente?

(Nota: algunas personas prefieren una aproximación en orden inverso, escribir test para definir el API y entonces escribir la implementación del código acorde a los tests. Esto es llamado Test- driven development o TDD)

Eligiendo tus abstracciones

Nosotros vamos a empezar a escribir un cliente para nuestra cola. Básicamente, El cliente “envía” un “trabajo” al “servidor”.

que estamos testeando exactamente?

Con esta pregunta que nos planteamos estamos intentando averiguar como hacer específicamente el cliente? Hemos dicho que un cliente tiene un método sendJob() (que parece suficientemente fácil de testear no?) en el método se tiene una especie de conversación con el servidor donde el cliente pregunta sobre el trabajo a realizar.

La conversación podría ser más o menos así

<!--?php class JobClient { const JOB_BATCH_SIZE = 20; /** * @var array the current batch of jobs, to be sent to the server when there * are JOB_BATCH_SIZE of them */ private $currentJobBatch = array(); /** * Tells this client to send off a job for execution. Jobs may be queued * for batch sending. */ public function sendJob($jobName, $jobParametersJson) { $this->currentJobBatch[] = [$jobName, $jobParametersJson];

if (count($this->currentJobBatch) + 1 >= self::JOB_BATCH_SIZE) {
$curlHandle = curl_init("jobserver.etsy.com/submit");
curl_setopt($curlHandle, CURLOPT_POST, 1);
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, ["data" => $this->currentJobBatch]);
$result = curl_exec($curlHandle);
$this->currentJobBatch = array();
return $result;

}
}
}

Entonces para testear tenemos que: a) encontrar una manera de mock era la llamada curl y b) más en profundidad, hemos limitado la clase JobClient a una decisión de implementación –que utilizará HTTP para enviar trabajos a utilizando la red — algo que en JobClient no nos interesa.

En otro orden de cosas, viendo el código parece que lo que tenemos aquí es un script, no código estructurado. Esto no es algo manejable en un sistema abstracto – de hecho solo ejecutar algunos comandos. Los script a son difíciles de testar y tener scripts no es el propósito cuando estamos construyendo un sistema abstracto. Así que para a
Dejar atrás el script podemos hacer algo como esto:

<!--?php class JobClient { public function sendJob($jobName, $jobParametersJson) { if (count($this->currentJobBatch) + 1 >= self::JOB_BATCH_SIZE) {
(new HttpJobTransmitter("jobserver.etsy.com"))->transmitJobs($this->currentJobBatch);
$this->currentJobBatch = array();
} else {
$this->currentJobBatch[] = [$jobName, $jobParametersJson];
}
}
}

Ahora hemos llegado a tener el trabajo serial izado con tan sólo una petición HTTP, la cual tiene suficiente entidad como para modelarse con su propia clase. Pero nuestro JobClient aún esta obligado a utilizar HTTP porque esto se decide dentro del método sendJob(). Elegir el protocolo de envío no es una responsabilidad de de JobClient. Aquí mostramos como solucionar esto.

<!--?php class JobClient { public function __construct(JobTransmitter $transmitter) { $this->transmitter = $transmitter;
}

public function sendJob($jobName, $jobParametersJson) {
if (count($this->currentJobBatch) + 1 >= self::JOB_BATCH_SIZE) {
$this->transmitter->transmitJobs($currentJobBatch);
$this->currentJobBatch = array();
} else {
$this->currentJobBatch[] = [$jobName, $jobParametersJson];
}
}
}

Debemos tener en cuenta que JobTransmitter_ es una interfaz.
Llegados a este punto podemos testar la implementación de nuestro JobClient utilizando un mock para JobTransmiter, lo que nos permitirá tener todos los casos para cuando el trabajo falle, sea correcto…

Eligiendo las correctas abstracciones y la correcta estructura para tu proyecto supondrá que cada una de las cosas que hagas — testing, mantenimiento, añadir funcionalidades, usabilidad — sean fáciles o difíciles.

ejercicios

  • ¿Cómo testeamos el proceso de trabajo en el cliente? ¿Qué pasa si no queremos que se ejecute el proceso para un test en particular?
  • ¿Qué pasaría si en vez de escribir una nueva clase HttpJobTransmitter nosotros hubiésemos escrito diferentes tipos de clientes, encapsulando HttpJobClient en cada uno de ellos? ¿Qué otras clases necesitaría este enfoque del sistema JobClient? ¿Crees que este enfoque es mejor que el anterior? ¿Que opinas del método sendJob()? Si no te gusta ¿cómo lo mejorarías?

Hasta aquí la primera parte del artículo. poco a poco intentaré terminarlo

Anuncios

Comenta la entrada

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s