Testing Django with Cypress, how nice!

In this post I share some recipes for testing Django with Cypress, with a focus on the authentication flow.

Testing Django with Cypress, how nice!

When I discovered Cypress in 2017 my life as a developer changed. I was not afraid to write functional tests anymore, and since then I applied this tool to any web framework I worked with.

In particular, I've been working almost exclusively with Django these days, and even if JavaScript does not fare so well in Python developers circle, when it comes to testing a Django/JavaScript project my tool of choice is always Cypress.

In this post I share a couple of recipes for testing Django with Cypress, with a focus on the authentication flow.

Testing Django login with Cypress

As suggested in the Cypress documentation, you may want to test the authentication flow of a web application no more than once with the UI.

This means you create a single test somewhere, where you check your login page:

describe("Login", () => {
  before(() => {
    cy.fixture("users.json").as("mockedUsers");
  });

  it("Can login through the UI", function () {
    cy.visit("/login/");
    cy.get("input[name='username']").type(this.mockedUsers[0].fields.email);
    cy.get("input[name='password']").type("dummy_password");
    cy.get("form").submit();
    cy.getCookie("sessionid").should("exist");
  });
});

As for the fixtures, you can use the same data from Django dumpdata, aply saved in cypress/fixtures/fixture_name.json.

Testing Django login without the UI

So far so good for the first login test. What if you now need to test other authenticated sections of the website?

Subsequent tests which require authentication should not use the UI again. What you should do instead is use cy.request() from Cypress, and preserve session cookies.

In all the other tests requiring authentication you do something like this:

describe("Authenticated sections", () => {
  before(() => {
    cy.fixture("users.json").as("mockedUsers");

    cy.visit("/login/");
    cy.get("[name=csrfmiddlewaretoken]")
      .should("exist")
      .should("have.attr", "value")
      .as("csrfToken");

    cy.get("@csrfToken").then((token) => {
      cy.request({
        method: "POST",
        url: "/login/", 
        form: true,
        body: {
          username: "juliana.crain@dev.io",
          password: "dummy_password",
        },
        headers: {
          "X-CSRFTOKEN": token,
        },
      });
    });

    cy.getCookie("sessionid").should("exist");
    cy.getCookie("csrftoken").should("exist");
  });

  beforeEach(() => {
    Cypress.Cookies.preserveOnce("sessionid", "csrftoken");
  });

  it("should do something", () => {
    // your test here
    // it's authenticated from now on!
  });

  it("should do something", () => {
    // your test here
    // it's authenticated from now on!
  });

});

What's happening here?

  • we save the CSRF Django token with .as("csrfToken").
  • we use cy.request() to make a POST request of type x-www-form-urlencoded by passing also the token in the headers as "X-CSRFTOKEN": token

This is way faster than using the UI over and over again to login.

Next up, we preserve cookies on each subsequent test with:

  beforeEach(() => {
    Cypress.Cookies.preserveOnce("sessionid", "csrftoken");
  });

All the tests now are authenticated and will send the cookies on each request to the backend.

UPDATE: starting from Cypress 8.2.0, we can now use cy.session() to cache and restore cookies as described in the documentation.

Django testserver meets Cypress

Django has a testserver command which you can use to start an ephemeral test database:

python manage.py testserver cypress/fixtures/users.json --noinput

The fixtures are obtained from dumpdata, and since they're JSON, they can be used by Cypress as well. You can load many fixture files at once.

Mocking the backend

In the "past" (which is 2 or 3 months in the JavaScript land) Cypress used an experimental Fetch polyfill for mocking Fetch calls.

They now came up with a new API, called intercept(). If your Django templates make XHR requests, you can avoid touching the backend altogether by stubbing the response. Here's an example:

  it("Can close tickets", () => {
    cy.get(".ticket__state")
      .should("have.class", "bg-yellow-200")
      .and("have.text", "Opened");

    cy.intercept("POST", "tickets", {
      statusCode: 200,
      ok: true,
    });

    cy.get("#close-ticket").click();

    cy.get(".ticket__state")
      .should("have.class", "bg-blue-200")
      .and("have.text", "Closed");
  });

Resources

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!

More from the blog: