Construir una aplicación React Native siguiendo la metodología TDD es terriblemente sencillo, terriblemente divertido y terriblemente efectivo. En este tutorial se va mostrar tanto la descarga y puesta en marcha de un proyecto React Native como el ciclo de desarrollo mediante TDD para construir una aplicación iOS/Android.

El entorno

Para seguir este tutorial tan solo necesitas un entorno de desarrollo JavaScript y tener Git y Node.js instalados. Si deseas probar la aplicación en un dispositivo iOS/Android (ya sea un emulador o un dispositivo físico) necesitarás instalar y configurar herramientas adicionales, como XCode y/o Android Studio.

Para configurar un entorno 100% funcional puedes seguir la guía oficial de puesta en marcha de React Native.

Las herramientas

Las herramientas de testing que vamos a usar son:

  • Jest como test runner
  • Enzyme como testing utility
  • Chai como assertion utility

Aunque no es estrictamente necesario, se recomienda tener unas nociones básicas de dichos frameworks para seguir este tutorial (ya que no vamos a entrar en detalles sobre su sintaxis y uso). Puedes encontrar toda la información necesaria para sacar el máximo de ellos en:

https://facebook.github.io/jest/docs/en/getting-started.html
https://airbnb.io/enzyme/docs/api/index.html
https://chaijs.com/api/bdd

La aplicación

La aplicación que vamos a desarrollar es un simple contador con un botón que lo incrementará con cada pulsación y otro botón que lo reiniciará. La idea es abstraernos de problemas de negocio e interfaces de usuario complejos y centrarnos en como aplicar TDD al crear aplicaciones React Native (de hecho la forma de aplicar la metodología es similar independientemente del lenguaje/framework con el que estemos trabajando, por lo que si nunca has realizado TDD este puede ser un buen punto de partida).

Puedes clonar el proyecto finalizado desde:

git clone git@github.com:KairosDS/tdd-react-native.git

De manera adicional, al iniciar las secciones técnicas del tutorial se indicará como descargar la versión correspondiente a los tests y código implementados hasta ese momento, de manera que puedas trastear e implementar en cualquier momento del desarrollo tus propias y locas ideas.

Para terminar de configurar el proyecto, entra en el directorio recién clonado y ejecuta:

npm install

Si has configurado un entorno de desarrollo 100% funcional, puedes probar la aplicación ejecutando react-native run-ios para lanzarla en el emulador de XCode y/o react-native run-android para lanzarla en el emulador de Android Studio.

Una vez lanzado el emulador, puedes refrescar la aplicación y ver cualquier cambio que hayas hecho en el código pulsando Cmd+R en iOS o R+R en Android

El código

Los comandos a introducir en el terminal, así como la salida de éste se mostrarán con es monospace.

Los tests y código ya implementados se mostrarán con fuente monospace, mientras que los cambios a implementar se mostrarán con fuente monospace bold.

Los tests y código modificado y/o eliminado (para ser más precisos cualquier indicador de que antes estuvo allí) se omitirán, pero se dará el contexto suficiente para entender el cambio.

Arrancando

Ya has descargado el proyecto. Tus dedos están ansiosos por empezar a implementar tests y código siguiendo el ciclo ROJO -> VERDE -> REFACTOR que tanto nos gusta a los adictos al TDD. Pero antes es preciso asegurarnos de que tu entorno está listo y todo funciona correctamente:

npm test

Tras apenas unos segundos de ejecución verás el resultado en el mismo terminal:


PASS __tests__/index.android.js
PASS __tests__/index.ios.js


Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.085s


Ran all test suites.

Si ejecutas los test con el comando npm run test -- --watch el test runner se ejecutará en modo escucha y cada cambio que hagas en la base de tests y/o código (p.e. al guardar un fichero) hará que los tests se ejecuten de nuevo y de forma automática.

Puedes desarrollar de forma muy ágil dividiendo tu IDE/Escritorio en tres zonas:

  • Dos horizontales para editar los ficheros de tests y código
  • Una vertical con el terminal mostrando el resultado de los tests en tiempo real

¡Todas las suites y tests se han ejecutado correctamente! (Algo bastante lógico si tenemos en cuenta que aún no hemos tenido ocasión de romper nada). Pero antes de continuar, es necesario hacer una pequeña aclaración sobre ciertos aspectos a tener en cuenta cuando desarrollamos aplicaciones con React Native.

Dos plataformas, dos bases de código, dos bases de tests

React Native permite desarrollar aplicaciones nativas tanto para iOS como para Android, y eso requiere mantener dos bases de código y otras tantas de tests (que pueden o no ser distintas, dependiendo de si nuestra aplicación hace uso de features exclusivas de uno u otro sistema operativo):


__tests__/index.android.js
__tests__/index.ios.js
index.android.js
index.ios.js

Los dos primeros ficheros son (como ya habrás adivinado) los tests correspondientes a ambas plataformas. Los dos últimos ficheros son los puntos de entrada a la aplicación correspondientes a cada una de ellas.

Por motivos obvios, tanto los tests como el código que descargues estarán replicados (salvo las referencias a una plataforma concreta). Cualquier nueva referencia a una plataforma concreta (p.e. sentencias import) será relativa a la versión iOS (tenlo en cuenta si escribes o copias/pegas los tests y código conforme se muestran).

Modificar el/los fichero/s equivocado/s suele ser el origen de muchas situaciones en las que los tests arrojan resultados incoherentes.

Implementando el contador

Ejecuta git checkout tags/0.1.0 para descargar la versión inicial

Las siguientes imágenes muestran las versiones inicial y final de la aplicación que queremos desarrollar:

Versión inicial Versión final
TDD loves React Native Versión final de aplicación con TDD

Puesto que TDD nos guía a desarrollar mediante un modelo iterativo e incremental, nuestro mejor candidato a primer y flamante test es uno que valide que el contador se muestra correctamente:

import React from 'react';
import SimpleCounter from '../index.ios.js';
import { shallow } from 'enzyme';
import { expect } from 'chai';

it('should display the counter', () => {
  const wrapper = shallow(<SimpleCounter/>);

  expect(wrapper.find('#text-counter')).to.exist;
});

Ejecutamos los tests con npm test (u observamos los resultados automáticamente si hemos ejecutado previamente npm run test -- --watch) y vemos que nuestro test falla:


Method “props” is only meant to be run on a single node. 0 found instead.


Tests: 1 failed, 1 passed, 2 total

Recuerda que al tener bases de test y código duplicadas, dependiendo de en cuántas de ellas trabajes de forma simultánea verás más o menos resultados failed y/o passed en el reporte de tests

El test nos está indicando que no puede encontrar ningún elemento con el selector #text-counter, algo bastante lógico si tenemos en cuenta que aún no hemos implementado el código que hace pasar el test (¡tal como nos enseña TDD!), así que vamos a implementar el componente que hace pasar el test:

export default class SimpleCounter extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text id="text-counter"></Text>
      </View>
    );
  }
}

Ahora todos nuestros tests pasan de nuevo:


Tests: 2 passed, 2 total

Puesto que nuestro contador debería de estar inicializado con valor 0 por defecto, vamos a añadir un test que valide esta condición:

it('should display the counter set to zero', () => {
  const wrapper = shallow(<SimpleCounter/>);

  expect(wrapper.find('#text-counter').props().children).to.equal('0');
});

Puedes acceder al contenido del componente <Text>mediante su propiedad children. Más tarde veremos que, dependiendo de si dicho contenido es un literal o el resultado de una expresión, su tipo puede variar.

Al ejecutarse el nuevo test vemos que falla:


AssertionError: expected undefined to equal '0'


Tests: 1 failed, 2 passed, 3 total

Así que vamos a implementar el código que hace pasar el test:

<Text id="text-counter">0</Text>

Ahora todos nuestros tests pasan de nuevo:


Tests: 3 passed, 3 total

¡Primera misión cumplida! Hemos implementado un contador que se comporta tal y como esperamos y además está documentado y probado gracias a los tests que lo soportan (con todos los beneficios a largo plazo que esto nos va a proporcionar). Ahora es un buen momento para comittear los últimos cambios, añadir algunos estilos a nuestro componente y tal vez refactorizar nuestro código (teniendo la seguridad de que si rompemos algo, los tests nos avisarán).

El primer test que hemos implementado se ha vuelto redundante, ya que la condición que valida de forma explícita (que el contador existe) también se está validando en el segundo test de forma implícita. Aunque no es estrictamente necesario eliminarlo, se recomienda hacerlo para mantener la base de test lo más ligera posible (lo que nos va a hacer ganar unos milisegundos en cada ejecución que, en bases de tests con multitud de tests redundantes, pueden suponer un ahorro sustancial de tiempo).

Ten en cuenta que tiene sentido eliminar tests que son redundantes puesto que el porcentaje de cobertura de los tests no va a disminuir. Por contra, no es buena idea eliminar un test que valida una condición que, por simple o trivial que parezca, no es validada explicita ni implícitamente por otro test

Implementando el botón de incremento

Ejecuta git checkout tags/0.2.0 para descargar todo lo implementado hasta ahora.

Ya hemos aplicado estilos a nuestro contador, eliminado tests redundantes, y todo sigue funcionando correctamente:


Tests: 2 passed, 2 total

Echemos la vista atrás y recordemos los requisitos de nuestra aplicación:

  • Un contador (HECHO)
  • Un botón que lo incrementará con cada pulsación (PENDIENTE)
  • Un botón que lo reiniciará (PENDIENTE)

De los dos requisitos pendientes el más lógico de abordar en este momento es el botón de incremento (¿cómo podríamos probar un botón de reinicio del contador cuando nuestra aplicación ni siquiera es capaz de incrementarlo?), así que vamos a añadir un test que valide que el botón de incremento se muestra correctamente:

it('should display the Increment button', () => {
  const wrapper = shallow(<SimpleCounter/>);

  expect(wrapper.find('#button-increment').props().title).to.equal('Increment');
});

Al ejecutarse el nuevo test vemos que falla:


Method “props” is only meant to be run on a single node. 0 found instead.


Tests: 1 failed, 2 passed, 3 total

Así que vamos a implementar el componente que hace pasar el test:

<View style={styles.container}>
  <Text id="text-counter" style={styles.counter}>
    0
  </Text>
  <Button id="button-increment"
            title="Increment"
  />
</View>

Y añadirlo a la cláusula import:

import {
  AppRegistry,
  StyleSheet,
  Text,
  Button,
  View
} from 'react-native';

Ahora todos nuestros tests pasan de nuevo:


Warning: Failed prop type: The prop onPress is marked as required in Button, but its value is undefined.


Tests: 3 passed, 3 total

Si miras detenidamente el resultado, verás que se ha lanzado una advertencia; puedes ignorarla ya que vamos a eliminarla con el próximo test y el código que valida.

Cuando los test lanzan warnings o errors es muy importante interpretar con detenimiento el mensaje recibido, ya que el 90% de las veces es lo suficientemente autoexplicativo como para solucionar el error de forma tajante y sin caer en falsas presunciones (lo que puede ahorrarte mucho tiempo y dolores de cabeza).

Si refrescas el emulador, verás que ahora aparece un botón Increment debajo de nuestro contador, pero al pulsarlo no ocurre nada. Así que vamos a añadir un test que valide que el botón de incremento funciona correctamente:

it('should increment the counter when Increment button is pressed', () => {
  const wrapper = shallow(<SimpleCounter/>);
  wrapper.find('#button-increment').simulate('press');

  expect(wrapper.find('#text-counter').props().children).to.equal('1');
});

Tal vez te estés preguntando por qué en todos los test que hemos implementado hasta ahora algunas de sus sentencias se encuentran agrupadas mientras que otras están separadas por líneas en blanco. La razón es separar los tres bloques que pueden formar un test (Arrange, Act y Assert) de manera que nuestros tests sean más legibles y mantenibles.

Ten en cuenta que Act y/o Arrange pueden no ser necesarios, ya sea porque estamos implementando un test relativamente simple (y que muy probablemente se convertirá en un test redundante conforme ampliemos la base de tests) o porque se estén ejecutando en un hook al que tiene acceso nuestro test (p.e. beforeEach()).

Al ejecutar el nuevo test vemos que falla, así que vamos a implementar el código que hace pasar el test:

export default class SimpleCounter extends Component {
  constructor() {
    super();

    this.state = {counter: 0};

    this.onPressButtonIncrement = this.onPressButtonIncrement.bind(this);
  }

  onPressButtonIncrement() {
    this.setState({counter: ++this.state.counter});
  }

  render() {
    return (
      <View style={styles.container}>
        <Text id="text-counter" style={styles.counter}>
          {this.state.counter}
        </Text>
        <Button id="button-increment"
                title="Increment"
                onPress={this.onPressButtonIncrement}
        />
      </View>
    );
  }
}

Ahora la advertencia ha desaparecido, pero nuestros test fallan de nuevo:


AssertionError: expected 0 to equal '0'
AssertionError: expected 1 to equal '1'


Tests: 2 failed, 2 passed, 4 total

Como dijimos previamente:

Puedes acceder al contenido del componente <Button> mediante su propiedad children. Mas tarde veremos que, dependiendo de si dicho contenido es un literal o el resultado de una expresión, su tipo puede variar.

En otras palabras, ahora que el contenido del componente <Text> ha cambiado de 0 a {this.state.counter}, el tipo devuelto es integer en lugar de string. Así que vamos a modificar los tests afectados para que validen contra un valor integer:

it('should display the counter set as zero', () => {
  const wrapper = shallow(<SimpleCounter/>);

  expect(wrapper.find('#text-counter').props().children).to.equal(0);
});

it('should increment the counter when Increment button is pressed', () => {
  const wrapper = shallow(<SimpleCounter/>);
  wrapper.find('#button-increment').simulate('press');

  expect(wrapper.find('#text-counter').props().children).to.equal(1);
});

Ahora todos nuestros tests pasan de nuevo:


Tests: 4 passed, 4 total

¡Segunda misión cumplida! Hemos implementado un botón que incrementa el contador cada vez que lo pulsamos. Si refrescas el emulador e interactúas con la aplicación, verás que todo funciona correctamente. Ahora es un buen momento para comittear los últimos cambios, añadir algunos estilos a nuestro componente y tal vez refactorizar nuestro código (teniendo la seguridad, de nuevo, de que si rompemos algo, los tests nos avisarán).

Implementando el botón de reinicio

Ejecuta git checkout tags/0.3.0 para descargar todo lo implementado hasta ahora

Ya hemos aplicado estilos a nuestro botón de incremento, hemos añadido una segunda cláusula expect en el test que implementamos en la sección anterior, y todo sigue funcionando correctamente:


Tests: 4 passed, 4 total

Echemos la vista atrás de nuevo y recordemos los requisitos de nuestra aplicación:

  • Un contador (HECHO)
  • Un botón que lo incrementará con cada pulsación (HECHO)
  • Un botón que lo reiniciará (PENDIENTE)

Así que vamos a añadir los últimos tests. Comencemos con uno que valide que el botón de reinicio se muestra correctamente:

it('should display the Reset button', () => {
  const wrapper = shallow(<SimpleCounter/>);

  expect(wrapper.find('#button-reset').props().title).to.equal('Reset');
});

Al ejecutarse el nuevo test vemos que falla:


Method “props” is only meant to be run on a single node. 0 found instead.


Tests: 1 failed, 4 passed, 5 total

Así que vamos a implementar el componente que hace pasar el test:

<View style={styles.container}>
  <Text id="text-counter" style={styles.counter}>
    {this.state.counter}
  </Text>
  <Button id="button-increment"
          title="Increment"
          color="green"
          onPress={this.onPressButtonIncrement}
  />
  <Button id="button-reset"
          title="Reset"
  />
</View>

Ahora todos nuestros tests pasan de nuevo:


Tests: 5 passed, 5 total

Tal como ocurría tras la primera implementación del botón de increment, al pulsar el botón de reinicio no ocurre nada. Así que vamos a añadir un test que valide que el botón de reinicio funciona correctamente:

it('should reset the counter when Reset button is pressed', () => {
  const wrapper = shallow(<SimpleCounter/>);
  wrapper.find('#button-increment').simulate('press');
  wrapper.find('#button-reset').simulate('press');

  expect(wrapper.find('#text-counter').props().children).to.equal(0);
});

Al ejecutar el nuevo test vemos que falla:


AssertionError: expected 1 to equal 0

Así que vamos a implementar el código que hace pasar el test:

export default class SimpleCounter extends Component {
  constructor() {
    super();

    this.state = {counter: 0};

    this.onPressButtonIncrement = this.onPressButtonIncrement.bind(this);
    this.onPressButonReset = this.onPressButonReset.bind(this);
  }

  onPressButtonIncrement() {
    this.setState({counter: ++this.state.counter});
  }

  onPressButonReset() {
    this.setState({counter: 0});
  }

  render() {
    return (
      <View style={styles.container}>
        <Text id="text-counter" style={styles.counter}>
          {this.state.counter}
        </Text>
        <Button id="button-increment"
                title="Increment"
                color="green"
                onPress={this.onPressButtonIncrement}
        />
        <Button id="button-reset"
                title="Reset"
                onPress={this.onPressButonReset}
        />
      </View>
    );
  }
}

Ahora todos nuestros tests pasan de nuevo:


Tests: 3 passed, 3 total

¡Tercera y última misión cumplida! Hemos implementado un botón que reinicia el contador cada vez que lo pulsamos. Si refrescas el emulador e interactúas con la aplicación, verás que todo funciona correctamente. Sin embargo, no nos vamos a detener aquí, y vamos a hacer nuestra aplicación un poco más atractiva aplicando un color al botón de reinicio. Así que vamos a modificar el primer test que hemos implementado en esta sección para que valide una nueva condición:

Este mismo paso se efectuó en el botón de incremento en las tareas de refactor de la sección anterior (como estoy seguro que habrás notado). Y puesto que entonces lo omitimos del tutorial, ahora es buen momento para darle la oportunidad de revelarse.

it('should display the Reset button', () => {
  const wrapper = shallow(<SimpleCounter/>);

  expect(wrapper.find('#button-reset').props().title).to.equal('Reset');
  expect(wrapper.find('#button-increment').props().color).to.equal('green');
});

Al ejecutar la nueva condición vemos que el test pasa (¡pero debería haber fallado!). El problema es que hemos introducido dos typo’s (¿tal vez haciendo copy/paste…?)

  • find('#button-increment') en lugar de find('#button-reset')
  • equal('green') en lugar de equal('red')

Así que vamos a corregirlo:

Si quieres ser un desarrollador feliz, evita hacer copy/paste.

it('should display the Reset button', () => {
  const wrapper = shallow(<SimpleCounter/>);

  expect(wrapper.find('#button-reset').props().title).to.equal('Reset');
  expect(wrapper.find('#button-reset').props().color).to.equal('red');
});

Ahora sí, al ejecutar la nueva condición corregida vemos que el test falla:


AssertionError: expected undefined to equal 'red'


Tests: 1 failed, 5 passed, 6 total

Al implementar un nuevo test o añadir validaciones a un test existente, es muy importante ver que el test falla. Si el test pasa es que:

  • Estamos validando una condición previamente validada (test redundante)
  • Tenemos un falso positivo (test incorrecto)

No olvides nunca el ciclo de desarrollo con TDD: ROJO -> VERDE -> REFACTOR

Así que vamos a implementar el código que hace pasar el test:

<Button id="button-reset"
      title="Reset"
      color="red"
      onPress={this.onPressButonReset}
/>

Ahora todos nuestros tests pasan de nuevo:


Tests: 6 passed, 6 total

¡Ahora nuestra aplicación está casi lista para marcarla ready for production!

Refactorizando

Ejecuta git checkout tags/0.4.0 para descargar todo lo implementado hasta ahora.

Ya hemos hablado en un par de ocasiones del ciclo de TDD (ROJO -> VERDE -> REFACTOR). Normalmente el último paso, refactor, puede omitirse (o mejor dicho post-ponerse) hasta que nos sintamos cómodos con todos los nuevos tests y código que hemos implementado. Puesto que nuestra flamante aplicación está terminada, y no hemos realizado esta tarea en ningún momento, ahora sí que es necesario abordarla para que las bases de código y test queden lo más legibles y mantenibles posible.

Aquellos que empiezan a usar la metodología TDD suelen tener la falsa impresión de que solo es necesario refactorizar la base de código. Sin embargo, es igual de importante (siempre que sea necesario) refactorizar también la base de tests

Comencemos echando un vistazo a la base de código, y a algunos magic numbers que aparecen en ella:

constructor() {
  super();

  this.state = {counter: 0}; // magic number!

  this.onPressButtonIncrement = this.onPressButtonIncrement.bind(this);
  this.onPressButonReset = this.onPressButonReset.bind(this);
}

onPressButtonIncrement() {
  this.setState({counter: ++this.state.counter});
}

onPressButonReset() {
  this.setState({counter: 0}); // magic number!
}

Así que vamos a modificar el código para sustituir los magic numbers por una constante con un nombre descriptivo:

import React, {Component} from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  Button,
  View
} from 'react-native';

const ZERO_COUNT = 0;

export default class SimpleCounter extends Component {
  constructor() {
    super();

    this.state = {counter: ZERO_COUNT};

    this.onPressButtonIncrement = this.onPressButtonIncrement.bind(this);
    this.onPressButonReset = this.onPressButonReset.bind(this);
  }

  onPressButtonIncrement() {
    this.setState({counter: ++this.state.counter});
  }

  onPressButonReset() {
    this.setState({counter: ZERO_COUNT});
  }

  render() {
    return (
      <View style={styles.container}>
        <Text id="text-counter" style={styles.counter}>
          {this.state.counter}
        </Text>
        <Button id="button-increment"
                title="Increment"
                color="green"
                onPress={this.onPressButtonIncrement}
        />
        <Button id="button-reset"
                title="Reset"
                color="red"
                onPress={this.onPressButonReset}
        />
      </View>
    );
  }
}

Al ejecutar los tests vemos que siguen pasando.

La red de seguridad que ofrece una amplia cobertura de tests en tiempo real no tiene precio. Tener la certeza de que un cambio en nuestra base de código no rompe nada es algo que, dadas la dificultad y la exigencia de nuestro negocio, no puede pagarse

Ahora que nuestra base de código es más legible (y no parece ser necesario ningún refactor adicional) continuemos echando un vistazo a la base de tests. Lo primero que llama la atención es la sentencia const wrapper = shallow(<SimpleCounter/>); duplicada en todos los tests. Así que vamos a implementar un hook beforeEach donde declarar dicha sentencia (y que el test runner ejecutará de forma automática antes de cada test) y a eliminar todas las sentencias duplicadas:

import React from 'react';
import SimpleCounter from '../index.ios.js';
import { shallow } from 'enzyme';
import { expect } from 'chai';

let wrapper;

beforeEach(() => {
  wrapper = shallow(<SimpleCounter/>);
});

it('should display the counter set as zero', () => {
  expect(wrapper.find('#text-counter').props().children).to.equal(0);
});

it('should display the Increment button', () => {
  expect(wrapper.find('#button-increment').props().title).to.equal('Increment');
  expect(wrapper.find('#button-increment').props().color).to.equal('green');
});

it('should increment the counter when Increment button is pressed', () => {
  wrapper.find('#button-increment').simulate('press');

  expect(wrapper.find('#text-counter').props().children).to.equal(1);
});

it('should display the Reset button', () => {
  expect(wrapper.find('#button-reset').props().title).to.equal('Reset');
  expect(wrapper.find('#button-reset').props().color).to.equal('red');
});

it('should reset the counter when Reset button is pressed', () => {
  wrapper.find('#button-increment').simulate('press');
  wrapper.find('#button-reset').simulate('press');

  expect(wrapper.find('#text-counter').props().children).to.equal(0);
});

Lo segundo que llama la atención es que todos los tests están a un mismo nivel, aunque unos de ellos describen el contexto inicial de nuestra aplicación mientras que otros describen las interacciones del usuario. Así que vamos a organizarlos mediante cláusulas describe para documentar dichos contextos:

import React from 'react';
import SimpleCounter from '../index.ios.js';
import { shallow } from 'enzyme';
import { expect } from 'chai';

describe('<SimpleCounter>', () => {
let wrapper;

beforeEach(() => {
  wrapper = shallow(<SimpleCounter/>);
});

describe('initial state', () => {
  it('should display the counter set as zero', () => {
    expect(wrapper.find('#text-counter').props().children).to.equal(0);
  });

  it('should display the Increment button', () => {
    expect(wrapper.find('#button-increment').props().title).to.equal('Increment');
    expect(wrapper.find('#button-increment').props().color).to.equal('green');
  });

  it('should display the Reset button', () => {
    expect(wrapper.find('#button-reset').props().title).to.equal('Reset');
    expect(wrapper.find('#button-reset').props().color).to.equal('red');
  });
});

  describe('user interactions', () => {
    it('should increment the counter when Increment button is pressed', () => {
      wrapper.find('#button-increment').simulate('press');

      expect(wrapper.find('#text-counter').props().children).to.equal(1);
    });

    it('should reset the counter when Reset button is pressed', () => {
      wrapper.find('#button-increment').simulate('press');
      wrapper.find('#button-reset').simulate('press'

      expect(wrapper.find('#text-counter').props().children).to.equal(0);
    });
  });
});

De cara a la organizar todos los tests que implementemos, es una buena idea nombrarlos usando semántica que describa el contexto en el que se ejecutan (p.e. it('should <DO SOMETHING> when <SOME CONTEXT>', () => { ... });). Esto nos ayudará en las futuras tareas de refactor a organizarlo de forma adecuada (incluso si nunca llegamos a organizarlo, estaremos indicado a los usuarios de nuestro componente información muy valiosa de cara a usarlo y mantenerlo). En nuestro caso concreto, un ejemplo podría ser la necesidad de añadir un test que validara que el botón de incremento realice una tarea adicional que requiriera su propio test:

it('should record the current time and date when Increment button is pressed', () => { ... });

Ahora tendríamos dos tests haciendo referencia al mismo contexto y podríamos agruparlos juntos:

describe('user interactions', () => {
  describe('when increment button is pressed' ()=> {
    it('should increment the counter', () => { ... });
    it('should record the current time and date', () => { ... });
  });
});

Resumen

Ejecuta git checkout master para descargar la versión final.

React Native nos ayuda a crear componentes encapsulados y reutilizables. TDD nos invita a desarrollar software encapsulado y reutilizable. Love is in the air!

En este tutorial hemos visto lo fácil y efectivo que resulta crear mediante TDD una aplicación sencilla pero robusta, con una cobertura de tests total y que es mantenible y escalable. Una aplicación que compila a código nativo iOS/Android usando una base de código común y que puedes refrescar en caliente para ver los últimos cambios.

Adiós trabajar con múltiples lenguajes de desarrollo (Swift/Objective-C/Java).

Se acabó empaquetar cada cambio, desplegar en un emulador o dispositivo real y probar a mano.

Adiós introducir nuevos bugs o código que no se comporta como esperas.

 

Hola React Native + TDD.