Azure Native Qumulo ahora disponible en la UE, el Reino Unido y Canadá: Más información

Cómo probamos nuestra UI React / Redux, y por qué nos ayuda a movernos rápidamente

Escrito por:

[et_pb_section bb_built = ”1 ″] [et_pb_row] [et_pb_column type =” 4_4 ″] [et_pb_text _builder_version = ”3.12.1 ″]

[Nota del autor: esta publicación de blog acompaña a la reunión de Seattle React.js que organizamos el 19 de septiembre de 2018, ver la grabación]

En Qumulo, construimos un sistema de archivos distribuidos de escalamiento horizontal, y resulta que construir un sistema de este tipo es difícil.

Tenemos capas sobre capas de subsistemas, todos trabajando juntos para formar el producto que llamamos Qumulo File Fabric. En general, el sistema es complejo y nuestros clientes dependen de él para almacenar sus datos valiosos.

Como ingenieros de Qumulo, trabajamos en un entorno de integración continua (CI), y las pruebas automatizadas son esenciales para garantizar que todo el código que desarrollamos funcione correctamente. Podemos estar seguros de que los datos de nuestros clientes están seguros incluso cuando cambiamos y desarrollamos activamente el software.

De abajo hacia arriba, nos preocupamos profundamente por las pruebas. Y en la parte superior de la pila, la interfaz de usuario es un lugar muy exclusivo donde los requisitos del usuario se exponen directamente. Hemos repetido muchas formas diferentes de probar nuestra interfaz de usuario de React / Redux. Me gustaría compartir a dónde nos llevó nuestra experiencia.

Las pruebas automatizadas son una inversión digna

El acto de escribir pruebas es caro. Mi experiencia anterior ha sido que, a pesar de que existe un objetivo comercial para tener una "buena cobertura de pruebas", también existe la presión de entregar más rápidamente, y terminamos escribiendo algunas pruebas de humo automatizadas aquí y allá, ya que pagamos dinero feliz Control de calidad para probar manualmente. ¡Diablos, incluso poblaron y administraron la base de datos de errores por nosotros!

En Qumulo, aprendí que las pruebas automatizadas son una gran inversión para el largo plazo. Cambiamos y refactorizamos el código a lo largo del tiempo, y cada vez que cambiamos el código, las pruebas nos protegen de cambiar inadvertidamente el comportamiento del sistema. Con nuestra cobertura de prueba completa, nuestra migración actual de ES6 a TypeScript como ejemplo se ha desarrollado sin problemas, por lo que podemos enfocarnos en la migración en lugar de verificar manualmente que la migración del código se realiza correctamente.

En el mundo de JavaScript, las bibliotecas de software pueden cambiar muy rápidamente, y algunas veces la actualización de una biblioteca es muy perjudicial. Nuestro conjunto de pruebas nos permite tener tranquilidad cuando actualizamos las bibliotecas, porque esperamos que los cambios de última hora también rompan nuestras pruebas de interfaz de usuario. Nuestros años de inversión en pruebas nos permiten realizar estas actualizaciones rápidamente para poder concentrarnos en el desarrollo y ahorrar mucho tiempo y energía con cada cambio de código.

La piramide de prueba

Hoy, escribimos nuestro código UI en React y Redux. En el nivel más pequeño, podemos imaginar que necesitamos pruebas unitarias para estas piezas - Reaccionar componentes, Redux reductores, creadores de acción, selectores.

Los juntamos para formar páginas, luego juntamos las páginas para construir la aplicación. Nuestro pruebas de integración siga la misma estructura: escribimos pruebas de página para verificar que el flujo de usuarios a través de una página sea el esperado, y escribimos de extremo a extremo pruebas del sistema para asegurarnos de que todo nuestro sistema funciona en conjunto. Esto encaja muy bien con La pirámide de prueba, conceptualizada por Martin Fowler en 2012..

La pirámide de pruebas sugiere que escribamos muchas pruebas unitarias, algunas pruebas de página y algunas pruebas de extremo a extremo. Cuando las pruebas de unidad fallan, deberían acercarnos mucho a la línea de código que falló; las pruebas de página deben fallar cuando nuestras partes React y Redux están haciendo suposiciones incorrectas entre sí; y las pruebas de extremo a extremo deberían fallar si nuestra interfaz de usuario está haciendo suposiciones incorrectas sobre nuestro backend.

Reaccionar unidad de prueba

En el pasado, probamos nuestros componentes React de una manera muy ingenua. Representamos el componente en el navegador y verificamos el DOM para ver si se procesó correctamente.

A medida que nuestro código base creció, las dependencias se convirtieron en un gran problema. Por ejemplo, si un componente reutilizado comúnmente se actualiza para acceder al almacén de Redux, es probable que todas las pruebas para sus usuarios deban actualizarse. Una solución podría ser probar todo con una tienda Redux provista, pero eso aumenta nuestro alcance de prueba (casi siempre queremos burlarnos de la tienda Redux como inyección de dependencia). Además, estas fallas de prueba no nos ayudan en nuestro desarrollo. La tienda Redux se proporciona a nivel de la aplicación, por lo que estas fallas de prueba apuntan a un error en la prueba y no en nuestro producto, lo que significa que dedicamos nuestro tiempo a realizar pruebas.

Tuvimos que aprender a tener muy claro qué es una unidad. En un juego que desarrollé para ilustrar las mejores prácticas y pruebas de React, los componentes están en capas como tales:

Consideremos las pruebas unitarias para ClickGame:</var/www/wordpress> the orange arrows are the inputs and outputs. When we consider this diagram closely, we realize that if we use a renderizador superficial, the inputs to a component are props and events, and the outputs are shallow rendering and event props. We can then focus on manipulating the props and events, and verify the shallow rendering and events generated:

importar * como Reaccionar desde "reaccionar"; importar {createRenderer} desde "react-test-renderer / shallow"; describe ("ClickGame", function () {beforeEach (function () {this.shallowRender = (gameState: ClickGameState) => {this.resetGameSpy = jasmine.createSpy ("resetGame"); this.shallowRenderer.render (  ); devuelve this.shallowRenderer.getRenderOutput (); }; this.shallowRenderer = createRenderer (); }); afterEach (function () {this.shallowRenderer.unmount (); testutil.verifyNoLogOutput ();}); describe ("no iniciado", function () {beforeEach (function () {this.gameState = new ClickGameState (); this.gameState.gameState = GameState.NotStarted; this.renderOutput = this.shallowRender (this.gameState);} ); it ("debería mostrar el indicador de inicio del juego", function () {esperar (this.renderOutput) .toHaveShallowRendered ( Un juego de clics: {"Click to start"} ); }); it ("debe reiniciar el juego al hacer clic en el botón", function () {const buttons = scryDomNodesInShallowRenderTreeByType (this.renderOutput, "button"); espera (buttons.length) .toEqual (2); const button = botones [2] como React .ReactHTMLElement ; button.props.onClick (jasmine.createSpy ("buttonEvent") como cualquiera); esperar (this.resetGameSpy) .toHaveBeenCalledWith (); }); }); });

Se utilizó reaccionar-redux para ayudarnos a conectar el componente a Redux, y connect () proporciona un miembro estático WrappedComponent que es el componente original que implementamos. Pruebas unitarias WrappedComponent nos permite simular directamente Redux accediendo directamente a los accesorios gestionados por reaccion-redux.

Pruebas unitarias redux

Probar el Redux básico es bastante sencillo.

[/et_pb_text][et_pb_accordion _builder_version=”3.12.1″][et_pb_accordion_item _builder_version=”3.12.1″ title=”Creador de acciones” use_background_color_gradient=”off” background_color_gradient_start=”#2b87da” background_color_gradient_end=”#29c4a9″ background_color _gradient_type=”lineal ” background_color_gradient_direction=”180deg” background_color_gradient_direction_radial=”center” background_color_gradient_start_position=”0%” background_color_gradient_end_position=”100%” background_color_gradient_overlays_image=”off” parallax=”off” parallax_method=”on” background_size=”cover” background_position=”center” background_repeat= ”no-repeat” background_blend=”normal” allow_player_pause=”off” background_video_pause_outside_viewport=”on” text_shadow_style=”none” box_shadow_style=”none” text_shadow_horizontal_length=”0em” text_shadow_vertical_length=”0em” text_shadow_blur_strength=”0em”]

Verifique que el creador de la acción devuelva la acción correcta con la carga útil esperada:

describe ("clickGameClick", function () {it ("debe crear una acción de clic", function () {espera (ClickGameActions.clickGameClick (2, 3)). toEqual ({payload: {col: 3, row: 2} , escriba: ClickGameActionType.Click});}); it ("debe lanzar si el índice de fila no es válido", function () {esperan (() => ClickGameActions.clickGameClick (-1, 0)). toThrow ();} ); it ("debería lanzar si el índice col no es válido", function () {esperan (() => ClickGameActions.clickGameClick (0, -1)). toThrow ();});});

3.12.1 2 grados” background_color_gradient_direction_radial=”centro” background_color_gradient_start_position=”87%” background_color_gradient_end_position=”29%” background_color_gradient_overlays_image=”off” parallax=”off” parallax_method=”on” background_size=”cover” background_position=”center” background_repeat=”no-repeat” background_blend=”normal” allow_player_pause =”desactivado” background_video_pause_outside_viewport=”activado” text_shadow_style=”ninguno” box_shadow_style=”ninguno” text_shadow_horizontal_length=”4em” text_shadow_vertical_length=”9em” text_shadow_blur_strength=”180em”]

Dado un estado inicial y una acción relevante, verifique que el reductor devuelve el estado esperado:

it ("debería reducir un clic a DoNotClick", function () {this.startState.getButton (1, 2) .state = ButtonGameState.Click; const newState = clickGameReducer (this.startState, ClickGamePlainActions.clickGameClick (1, 2)) ; espera (newState) .not.toBe (this.startState, "necesita haber creado un nuevo objeto de estado"); espera (newState.gameState) .toEqual (GameState.Started); espera (newState.getButton (1, 2) .state) .toEqual (ButtonGameState.DoNotClick); espera (newState.score) .toEqual (1);});

3.12.1 2 grados” background_color_gradient_direction_radial=”centro” background_color_gradient_start_position=”87%” background_color_gradient_end_position=”29%” background_color_gradient_overlays_image=”off” parallax=”off” parallax_method=”on” background_size=”cover” background_position=”center” background_repeat=”no-repeat” background_blend=”normal” allow_player_pause =”desactivado” background_video_pause_outside_viewport=”activado” text_shadow_style=”ninguno” box_shadow_style=”ninguno” text_shadow_horizontal_length=”4em” text_shadow_vertical_length=”9em” text_shadow_blur_strength=”180em”]

Dado un estado de Redux, el selector de verificación devuelve el valor correcto:

it ("debería devolver falso si el botón no se puede encontrar en un ClickGameState", function () {const state: HandbookState = {clickGame: null}; espera (ClickGameSelector.getButtonState (state, 0, 0)). toBeFalsy ();} ); it ("debe devolver el estado del juego de clic del estado del manual global", función () {const clickGameState = jasmine.createSpy ("estado de ClickGame") como cualquiera; estado const: HandbookState = {clickGame: clickGameState}; esperar (ClickGameSelector. getClickGameState (estado)) .toBe (clickGameState);});

[/et_pb_accordion_item][et_pb_accordion_item _builder_version=”3.12.1″ title=”Thunked action Creator” use_background_color_gradient=”off” background_color_gradient_start=”#2b87da” background_color_gradient_end=”#29c4a9″ background_color_gradient_type=”linear” background_color_gradient_direction= ”180 grados” background_color_gradient_direction_radial=” center” background_color_gradient_start_position=”0%” background_color_gradient_end_position=”100%” background_color_gradient_overlays_image=”off” parallax=”off” parallax_method=”on” background_size=”cover” background_position=”center” background_repeat=”no-repeat” background_blend=”normal ” allow_player_pause=”off” background_video_pause_outside_viewport=”on” text_shadow_style=”none” box_shadow_style=”none” text_shadow_horizontal_length=”0em” text_shadow_vertical_length=”0em” text_shadow_blur_strength=”0em”]

El creador de acciones de Thunked nos ayuda a enviar múltiples acciones mientras esperamos que se complete una función asíncrona, generalmente una llamada a la API REST.

En el espíritu de la prueba unitaria, asumimos que los creadores de acción simple ya están probados. Para el creador de acciones thunked, controlamos el resultado de la función asíncrona y esperamos que se envíe el conjunto correcto de acciones. Podemos hacer esto usando una tienda simulada de Redux. En este ejemplo, la función asíncrona es un JavaScript. setTimeout</var/www/wordpress>:

describe ("clickGameStartRound", function () {beforeEach (function () {jasmine.clock (). install (); this.gameState = new ClickGameState (); spyOn (ClickGameSelector, "getClickGameState") .and.returnValue (this. gameState); this.mockStore = new ReduxMockStore ({});}); afterEach (function () {jasmine.clock (). uninstall ();}); it ("no debería enviarse si el juego no ha comenzado", function () {this.gameState.gameState = GameState.NotStarted; this.mockStore.dispatch (ClickGameActions.clickGameNewRound ()); espera (this.mockStore.getActions ()) .toEqual ([], "Se espera que la ronda no se envíe correctamente lejos "); jasmine.clock (). tick (3001); espera (this.mockStore.getActions ()). toEqual ([]," No se esperan rondas nuevas ");}); it (" debería enviar una nueva ronda cada 3 segundos cuando el juego ha comenzado ", function () {this.gameState.gameState = GameState.Started; this.mockStore.dispatch (ClickGameActions.clickGameNewRound ()); espera (this.mockStore.getActions ()). ToEqual ([ ], "Espere que la ronda no se envíe de inmediato"); jasmine.clock ( ) .tick (3001); esperar (this.mockStore.getActions ()). toEqual ([{tipo: ClickGameActionType.NewRound}], "Espere que se envíe una nueva ronda después de 3 segundos"); this.mockStore.clearActions (); jazmín.clock (). tic (3001); esperar (this.mockStore.getActions ()). toEqual ([{tipo: ClickGameActionType.NewRound}], "Espere que se envíe una nueva ronda después de 6 segundos"); }); });

[/et_pb_accordion_item][/et_pb_accordion][et_pb_text _builder_version=”3.12.1″]

Pruebas de integración a nivel de página

Ahora que todas nuestras unidades están probadas, debemos asegurarnos de que encajan correctamente. El objetivo de las pruebas de integración a nivel de página (o "pruebas de página" para abreviar) es verificar que los componentes de React interactúen correctamente y que React y Redux funcionen juntos correctamente.

Herramientas que necesitamos

Hubo dos problemas que debemos resolver para escribir pruebas de página.

Necesitamos una forma de burlar nuestras llamadas de API REST. Creamos el AjaxManager que intercepta todas las llamadas a $ .ajax y proporciona métodos para hacer que una solicitud espere, tenga éxito o falle.

También necesitamos una forma de esperar programáticamente a que cambie nuestra interfaz de usuario antes de dar el siguiente paso en la prueba. Creamos TestStepBuilder, que es una herramienta que nos permite escribir pruebas que esperan que se cumplan las condiciones antes de tomar más pasos.

En el juego de demostración, la acción asíncrona se realiza en un temporizador, por lo que no hay ningún ejemplo del AjaxManager aquí, pero utiliza el TestStepBuilder para pasar por las pruebas:

beforeAll (function () {this.handbookPage = new HandbookPage (); this.handbookPage.render (hecho);} afterAll (function () {this.handbookPage.cleanUp ();}); it ("debería iniciar el juego de haciendo clic al hacer clic en el botón de inicio ", función (hecho) {new TestStepBuilder () .step (" Verificar que el juego no ha comenzado ", () => {esperar (this.handbookPage.getGameStatus ()). toEqual (" Haga clic para start ");}) .waitFor (" Botón de inicio ", () => {return this.handbookPage.findStartButton ();}) .step (" Haga clic en el botón de inicio ", () => {this.handbookPage.findStartButton () .click ();}) .waitFor ("ClickGameTable para renderizar", () => {return this.handbookPage.findClickGameTable ();}) .step ("Verificar que el juego está en progreso", () => { esperar (this.handbookPage.getGameStatus ()). toEqual ("En progreso ...");}) .run (hecho);}); it ("debe continuar el juego de hacer clic al hacer clic en un botón verde", function (hecho) {new TestStepBuilder () .step ("Haga clic en un botón verde", () => {esperar (this.handbookPage.getScore ()). toEqual (0, "la puntuación debe ser 0"); const $ greenButtons = this.handbookPage. $ findGreenButtons (); esperar ($ greenButtons.length) .toBeGreaterThan (0, "debería tener al menos 1 botón verde al reiniciar el juego"); $ greenButtons [0] .click (); }) .waitFor ("puntuación para subir", () => this.handbookPage.getScore ()> 0) .step ("Verificar puntuación y estado del juego", () => {esperar (this.handbookPage.getGameStatus ( )). toEqual ("En progreso ..."); espera (this.handbookPage.getScore ()). toEqual (1, "la puntuación debe ser 1");}). run (hecho); }); it ("debe finalizar el juego y mostrar el botón de reinicio cuando se hace clic en un botón rojo", función (hecho) {new TestStepBuilder () .step ("Haga clic en un botón rojo", () => {esperar (this.handbookPage.getScore ()). toEqual (1, "el puntaje debe ser 1"); const $ redButtons: JQuery = this.handbookPage. $ findRedButtons (); espera ($ redButtons.length) .toBeGreaterThan (0, "debe ser al menos uno rojo botón después de hacer clic en verde "); $ redButtons [0] .click ();}) .waitFor (" Botón de reinicio para mostrar ", () => {this.handbookPage.findRestartButton ();}) .step (" Verificar que el juego ha terminado ", () => {espera (this.handbookPage.getScore ()). toEqual (1," la puntuación debe permanecer 1 "); espera (this.handbookPage.getGameStatus ()). toEqual (" JUEGO OVER ");}). Ejecutar (hecho);});

Patrón de diseño de objeto de página

En el código de ejemplo anterior, notará que el código es ajeno a la implementación de la página.

Hicimos uso de un patrón de diseño documentado por Selenio , que son patrón de diseño de objeto de página. En el código de ejemplo, HandbookPage es un objeto de página que envuelve nuestra implementación del componente de página del manual React, y accedemos a la UI solo a través del objeto de página en nuestras pruebas.

Este desacoplamiento tiene dos ventajas.

  1. Hace que nuestras pruebas sean más fáciles de leer.
  2. Si alguna vez cambiamos la implementación de la página, solo necesitamos actualizar el objeto de la página y no las pruebas.

De esta manera, las pruebas de página solo describen cómo se debe probar la página, y los detalles de la implementación se encapsulan en el objeto de la página.

Pruebas de sistema de extremo a extremo

En las pruebas de sistema de UI de extremo a extremo, activamos un clúster de Qumulo y ejercitamos nuestra UI. Usamos las mismas herramientas que en nuestras pruebas de página, simulamos las acciones del usuario e inspeccionamos la interfaz de usuario usando objetos de página, y trabajamos a través de los pasos de prueba usando TestStepBuilder.

El objetivo del sistema es verificar que la API se está comportando correctamente mediante el ejercicio de la interfaz de usuario. Suele haber mucha superposición entre las pruebas de página y las pruebas de extremo a extremo. Por lo general, las pruebas de la página se centran en todos los diferentes eventos asíncronos posibles (como las desconexiones de la red), mientras que el sistema comprueba específicamente que la IU y la API REST están haciendo las suposiciones correctas entre sí.

Las pruebas nos ayudan a movernos rápido

Las pruebas nos ayudan a movernos rápido porque nos ayudan a producir menos errores, por lo que nuestro flujo de trabajo se interrumpe menos por la corrección de errores, lo que significa que nos centramos en un mayor desarrollo.

Durante el último año, nos alejamos de un modelo con una capa de pruebas de integración unitarias y una capa de pruebas de sistema. En este modelo antiguo, las pruebas "unitarias" tenían demasiadas dependencias, lo que las hace frágiles, mientras que las pruebas del sistema requerían demasiado esfuerzo para ejecutarse regularmente durante el desarrollo.

Aprendimos muchas lecciones de esto Google Testing blog post, que describe todas las razones por las que nos mudamos al nuevo modelo con tres capas de pruebas. Hoy, cuando una prueba de unidad falla, nos da información muy específica sobre lo que se rompió. El compilador de TypeScript se asegura de que nuestro código se ajuste de manera sintáctica y semántica, y las pruebas de la página verifican que nuestros componentes React y el código de Redux tengan las suposiciones correctas entre sí. Esto deja las pruebas del sistema para tener un trabajo mucho más pequeño para asegurar los contratos de la API REST en lugar de tratar de verificar la corrección en nuestros sistemas de UI.

Las buenas prácticas de prueba nos han ayudado a avanzar más rápido porque confiamos en nuestro código a medida que crece y evoluciona con el tiempo, y la forma en que dividimos las pruebas hoy en día las hace más fáciles de escribir y mantener, al tiempo que nos brinda información mucho más específica cuando las pruebas fallan. Continuamos buscando mejores formas de desarrollar nuestro código, ¡y esperamos compartir más a medida que continuamos aprendiendo!

[/ Et_pb_text] [/ et_pb_column] [/ et_pb_row] [/ et_pb_section]

Artículos Relacionados

Ir al Inicio