DEV Community

Cover image for Testing a Redux data store with Cypress.io
Damien Cosset
Damien Cosset

Posted on

Testing a Redux data store with Cypress.io

Introduction

In the previous article, we started to learn how to use Cypress to test our code, with a focus on React. In this article, we'll explore how to use Cypress with data stores, like Redux.

This article assumes that you've read the previous article, in order to install Cypress and launch it with your projects. A basic understanding of Redux is also a nice bonus.

You can find the full code on You can find the full code on Github.

The goals

The application we will test is rather straightforward. I can do a few things:

  • Open a modal with a form that allows me to add a contact to a list
  • Display a list of contacts
  • Delete contacts

I use Redux to keep track of my application state. Our objectives will be to test that our store in Redux properly manage our state changes.

This is what our application looks and works like.

What to test

Let's write down precisely what we want to test. Our app's state is as follow:

{ contacts: [], isModalCreationOpen: false }
  • When I click the Create Contact button, I want to make sure the boolean is set to true, indicating that the modal is open.
  • When I click on the Close button inside the creation modal. I want to make sure the boolean is set to false, indicating that the modal is closed.
  • When I click on the Close and Save button inside the creation modal, I want to make sure the boolean is set to false, indicating that the modal is closed. I also want to make sure that the contact has been added to the list of contacts.
  • When I click on the Delete Contact button, I want to make sure that the contact has been removed from the contact list.

Testing the modal functionality

Let's begin by testing if our modal works the way it should. I'll run the command yarn run cypress open (or $(npm bin)/cypress run with npm). If this is the first time you run Cypress in your project, Cypress will create a folder for you. Our tests will go into the newly created cypress/integration folder. Inside that folder, I'll create a contacts_app.spec.js file to write our test in.

The very first test we'll write is to make sure our app doesn't crash when I visit http://localhost:3000, where my app is displayed on my machine.

Note: For our tests to work, your React application must also be launched 😉. You should have Cypress running in one tab and your React application running in another.

So, our first test will be:

describe("LOAD OK", function() {
  it("should load without crashing", function() {
    cy.visit("http://localhost:3000");
  });
});

Test visit homepage

Great, now we can start testing the modal functionality. The first thing we need to do is to expose our store. We are going to add the following code at the end of the src/index.js file:

const store = createStore(rootReducer);
render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
//We add these 3 lines here
if (window.Cypress) {
  window.store = store;
}

What the last 3 lines do is check if we are in a Cypress environment. If that's the case, we assign our Redux store to the window object. That will allow us to access and test our data store inside Cypress. Note that if we are no in a Cypress environment, we don't touch the window object. So, other development and production environment are not affected by this code.

Now, by using cy.window().its('store').invoke('getState'), we can access our Redux store's state in our Cypress tests!

The first test we will write will check that our initial state is the correct one. We should have an object with an empty contacts array and a boolean set to false:

describe("LOAD OK", function() {
  it("should load without crashing", function() {
    cy.visit("http://localhost:3000");
  });

  it("should have the right initial state", function() {
    cy.window()
      .its("store")
      .invoke("getState")
      .should("deep.equal", { contacts: [], isModalCreationOpen: false });
  });
});

Initial Redux state test

All good, now let's open our modal. We need to get our button and use the click() method on it. After that, we'll check that isModalCreationOpen is set to true.

describe("Modal Open and Close", function() {
  it("should open the modal", function() {
    cy.get("button.open-modal-btn");
    cy.get("button").click();
    cy.window()
      .its("store")
      .invoke("getState")
      .its("isModalCreationOpen")
      .should("equal", true);
  });
});

Open modal passing test

Notice that we use the its() method to get the value of the key isModalCreationOpen inside our state object.

Next, we'll test that clicking on the Close button set the isModalCreationOpen boolean to false. There are two ways we can test this at this moment:

  • Because our state persists between tests, our modal is open. Which means I don't need to re-open it again. I can look for the Close button right away and click it.
  • I can decide to use a hook, like beforeEach() that runs before every tests in the suite. Inside that hook, I can use the dispatch function from Redux and call an action to close our modal. This way, I'm sure that every test in this suite starts with a closed modal.

I'll use a beforeEach() function hook here. The Redux action I am looking for has a CLOSE_CREATION_MODAL type:

describe("Modal Open and Close", function() {
  beforeEach(function() {
    cy.window()
      .its("store")
      .invoke("dispatch", { type: "CLOSE_CREATION_MODAL" });
  });
  it("should open the modal", function() {
    cy.get("button.open-modal-btn");
    cy.get("button").click();
    cy.window()
      .its("store")
      .invoke("getState")
      .its("isModalCreationOpen")
      .should("equal", true);
  });
});

Now, before every test, whatever is inside the beforeEach() function will be executed.

Now, we can write our closing modal test:

describe("Modal Open and Close", function() {
  beforeEach(function() {
    cy.window()
      .its("store")
      .invoke("dispatch", { type: "CLOSE_CREATION_MODAL" });
  });
  // Test for opening modal, cut for brevity
  it("should close the modal", function() {
    cy.get("button.open-modal-btn").click();
    cy.get("button.MuiButton-containedSecondary").click();
    cy.window()
      .its("store")
      .invoke("getState")
      .its("isModalCreationOpen")
      .should("equal", false);
  });
});

Awesome, now can move on to the second part: testing the contacts functionality.

Testing the contacts functionality

The first thing we will test is the contact creation functionality. We need to:

  • open the modal
  • type a name in the first input
  • type a phone number in the second input
  • click the Save and Close button
  • make sure our state has been update properly

Here is how you could do that:

describe("Create a contact functionality", function() {
  it("should create a contact", function() {
    cy.get("button.open-modal-btn").click();
    cy.get("input")
      .first()
      .type("Damien");

    cy.get("input")
      .last()
      .type("555-888-999");

    cy.get(".contact-creation-btn").click();

    cy.window()
      .its("store")
      .invoke("getState")
      .its("contacts")
      .should("have.length", 1);

    cy.window()
      .its("store")
      .invoke("getState")
      .its("contacts")
      .should("deep.equal", [
        { id: 0, name: "Damien", phone_number: "555-888-999" }
      ]);

    cy.window()
      .its("store")
      .invoke("getState")
      .its("isModalCreationOpen")
      .should("equal", false);
  });

I'm doing quite a few things here. After opening my modal, I retrieve the first input and use the type() function, to give the input the value Damien. Then, I retrieve the second input and give it the value 555-888-999.
Then, I click on the Close and Save button by using click(). Now, we can get into some assertions. I first test that the contacts array in my store state has a length of 1. I'm also testing if the array has the right content. It should have an object with the proper id, name and phone_number. Finally, I make sure that the modal is closed.

We can see in the preview, on the right side of the Cypress Test Runner, how the events are taking place.

Moar contacts!!!

Great, now let's add two more contacts.

describe("Create a contact functionality", function() {
  // I cut the should create a contact test for brevity
  it("should create 2 contacts", function() {
    cy.get("button.open-modal-btn").click();
    cy.get("input")
      .first()
      .type("John");

    cy.get("input")
      .last()
      .type("555-777-111");

    cy.get(".contact-creation-btn").click();

    cy.window()
      .its("store")
      .invoke("getState")
      .its("contacts")
      .should("have.length", 2);

    cy.get("button.open-modal-btn").click();
    cy.get("input")
      .first()
      .type("Jennifer");

    cy.get("input")
      .last()
      .type("555-000-333");

    cy.get(".contact-creation-btn").click();

    cy.window()
      .its("store")
      .invoke("getState")
      .its("contacts")
      .should("have.length", 3);

    cy.window()
      .its("store")
      .invoke("getState")
      .its("isModalCreationOpen")
      .should("equal", false);
  });
});

Open modal passing test

Remember that our store keeps its state between the two tests because we didn't do any cleanups with beforeEach() this time. So, after we add the second contact, the length of the contacts array should be 2. The length should be 3 once we add the third contact.

Delete a contact

Last thing we need to test is the contact deletion. We now have three contacts in our list. So, I'll remove the second one, John.

describe("Delete a contact functionality", function() {
  it("should delete a contact", () => {
    cy.get(".delete-contact-btn")
      .eq(1)
      .click();

    cy.window()
      .its("store")
      .invoke("getState")
      .its("contacts")
      .should("have.length", 2);
    cy.window()
      .its("store")
      .invoke("getState")
      .its("contacts")
      .should("deep.equal", [
        {
          id: 0,
          name: "Damien",
          phone_number: "555-888-999"
        },
        {
          id: 2,
          name: "Jennifer",
          phone_number: "555-000-333"
        }
      ]);
  });
});

Open modal passing test

The first thing we do is retrieve the button we want to click. To do that, we can use the eq() method and give it the index of our button. In our case, that index is 1 because we start counting from 0. We then call the click() method.

The contact should be deleted from the state. We first test that our contacts array has indeed a length of 2. Finally, we run another assertion to make sure that John is no longer in the contact array, while the data from the two other contacts has remained unchanged.

Conclusion

We've covered a lot of things in this article to help you understand how you could use Cypress to test your Redux store inside a React application. Cypress allows us to test the contents of our reducers, while also letting us use the dispatch function. Thanks to that, we have complete control of our data store for our testing purposes.

Have fun ❤️

Top comments (2)

Collapse
 
meatboy profile image
Meat Boy

Nice, I am going to try this approach one day. Seems to be more efficient than using shallow renderers. I really hate using traditional testing approaches with frontend since exporting and injecting things from/into components is meshy.

Collapse
 
keyboslice_44 profile image
Keir Lavelle

Nice to know about checking for the global Cypress and exposing parts of the application to the world based on that thanks, but I feel like these tests are brittle and testing the wrong thing.

We shouldn't really care if there's a isModalOpen flag in our store that is true, or if our array of contacts is a certain length - those are just implementation details, what we should care about is that the modal appears in the viewport in response to some interaction, or that the correct number of contact cards is displayed.

It's much more valuable to test the things that the store controls, not the store itself and you can also refactor more easily when you're testing the end result and not the implementation.

It's really useful to know how to expose the store to Cypress for the purposes of setting up the state for the test though, so thanks for that