How to write your first Cypress test

How to write your first Cypress test

When let’s face it, relying on manual clicking to test your application is like trusting a toddler with a loaded gun. Sure, it seems harmless at first, but one little slip and you’ve got chaos on your hands. Clicking around is subjective; it’s prone to human error, and frankly, it doesn’t scale. If you want to catch those pesky bugs before they make it to production, automated testing is where you need to focus your efforts.

Consider using a framework like Selenium for browser automation. Selenium can simulate clicking around, filling forms, and navigating through your app, but it does so with the consistency of a robot. Here’s a quick example of how you can set up a simple Selenium test:

const { Builder, By, Key, until } = require('selenium-webdriver');

(async function example() {
  let driver = await new Builder().forBrowser('firefox').build();
  try {
    await driver.get('http://your-website.com');
    await driver.findElement(By.name('q')).sendKeys('example', Key.RETURN);
    await driver.wait(until.titleIs('example - Google Search'), 1000);
  } finally {
    await driver.quit();
  }
})();

This snippet launches a Firefox browser, navigates to your website, enters “example” in a search box, and waits for the title to change. This is just scratching the surface, but it sets the stage for real automation.

Using tools like Selenium not only saves you time but also adds a layer of reliability to your testing. You can run these tests as part of your CI/CD pipeline, catching issues before they ever reach the user. Imagine waking up to find that your latest build has passed all tests overnight, all thanks to the robots doing the heavy lifting.

And let’s not forget about the importance of writing tests that mirror actual user behavior. If a user clicks this button, then that should appear, right? If your tests can cover these scenarios, you’re much less likely to end up embarrassed at a demo when something breaks.

A lot of developers resist writing tests because they feel it slows them down. But consider the cost of fixing bugs post-release versus investing a bit of time upfront to catch them in a controlled environment. The choice seems clear. So let’s cut the chatter about clicking and make our code do the talking instead. Build your tests, and trust me, you’ll feel the weight lift off your shoulders as you discover the freedom of reliable code.

The surprisingly painless five-minute setup

Now, let’s dive into the setup. Sure, it may sound daunting at first, but getting started with automated testing is surprisingly painless. You can have a functional testing setup in under five minutes if you follow the right steps. First, you’ll need Node.js installed on your machine. If you don’t have it yet, go to the Node.js website and grab the installer. Once you have Node.js, you can install Selenium WebDriver and the browser-specific driver. Here’s how you can do that:

npm install selenium-webdriver
npm install geckodriver --save-dev

With the WebDriver and browser driver installed, you can start writing your first test. This is the moment you start to realize that the setup was just a series of simple commands. Now you have all the tools you need to begin automating your tests.

Next, you might want to structure your tests in a way that’s easy to manage. Organizing your test cases is crucial. You can create a directory called tests and place your test files there. Here’s an example of a basic test structure:

tests/
  ├── example.test.js
  └── another.test.js

In example.test.js, you can write your first test like this:

const { Builder, By, Key, until } = require('selenium-webdriver');

describe('My First Test', () => {
  let driver;

  beforeAll(async () => {
    driver = await new Builder().forBrowser('firefox').build();
  });

  afterAll(async () => {
    await driver.quit();
  });

  test('should find the search box', async () => {
    await driver.get('http://your-website.com');
    const searchBox = await driver.findElement(By.name('q'));
    expect(await searchBox.isDisplayed()).toBe(true);
  });
});

This example uses Jest for structuring the tests and shows how easy it is to check if an element is present. The beforeAll and afterAll hooks ensure that your browser opens and closes appropriately. This makes your tests cleaner and easier to maintain.

Once you’ve got your tests written, running them is just as straightforward. You can execute your tests using a simple command line instruction. If you’re using Jest, you would run:

npx jest

What you’ll find is not just that your tests run quickly, but also that they give you immediate feedback on your application’s state. The speed of running tests and getting results will allow you to iterate rapidly, making your development process much more efficient.

As you begin to teach your automation how to use your website, you’ll want to consider various user scenarios. Think about the paths a user might take through your application. Each unique interaction is a chance to create a test that solidifies your application’s reliability. For instance, testing the login process is crucial:

test('should log in a user', async () => {
  await driver.get('http://your-website.com/login');
  await driver.findElement(By.name('username')).sendKeys('testuser');
  await driver.findElement(By.name('password')).sendKeys('password', Key.RETURN);
  await driver.wait(until.titleIs('Dashboard'), 1000);
});

In this example, you’re not just testing that the login form works; you’re ensuring that the entire flow from input to redirection is functioning as intended. This level of thoroughness is what builds trust in your code. And that trust is crucial when you’re working with a team or deploying to production.

It’s not just about having tests; it’s about having tests that mirror real user interactions. The more your tests reflect how actual users will interact with your application, the more confidence you can have in your code. You’ll find that as you iterate and add more tests, you begin to trust your application more, leading to a more efficient development cycle and ultimately a better product.

As you develop your tests, you might start to notice patterns and common issues in your application. This insight can drive improvements in both your code and your testing strategy. By focusing on these areas, you’ll not only enhance your testing capabilities but also your overall coding practices. It’s a positive feedback loop that reinforces good habits and sustainable development…

Teaching the robot how to use your website

So you’ve written a few tests. You’re finding elements, sending keys, and checking titles. It feels good. But after a while, you’ll notice something annoying. Your test files are getting long and repetitive. You have the same sequence of findElement and sendKeys for logging in copied into five different test files. Then, a developer changes the ID of the username field. Now you have to go find and replace that ID in all five files. This is not just tedious; it’s a recipe for disaster. You’ll miss one, a test will fail, and you’ll waste an hour figuring out why.

This is where you stop just writing scripts and start building a proper testing framework. The secret is to stop thinking about your tests as a linear sequence of commands and start thinking in terms of the pages of your application. This is called the Page Object Model (POM), and it’s not nearly as complicated as it sounds. It’s just a smart way to organize your code.

A Page Object is simply a class that represents a page (or a major component) of your web application. This class is responsible for two things: finding the elements on that page and encapsulating the actions a user can perform. Instead of putting driver.findElement(By.name('username')) directly in your test, you put it inside your LoginPage class.

Let’s refactor that login test. First, we create a Page Object for the login page. We’ll put this in its own file, say, pages/LoginPage.js.

const { By } = require('selenium-webdriver');

class LoginPage {
  constructor(driver) {
    this.driver = driver;
    this.url = 'http://your-website.com/login';
    this.usernameInput = By.name('username');
    this.passwordInput = By.name('password');
    this.submitButton = By.css('button[type="submit"]');
  }

  async navigate() {
    await this.driver.get(this.url);
  }

  async login(username, password) {
    await this.navigate();
    const userField = await this.driver.findElement(this.usernameInput);
    await userField.sendKeys(username);
    const passField = await this.driver.findElement(this.passwordInput);
    await passField.sendKeys(password);
    await this.driver.findElement(this.submitButton).click();
  }
}

module.exports = LoginPage;

Look at what we’ve done here. All the ugly details about how to find elements on the login page are now hidden away inside this class. We’ve also created a high-level login method that performs the entire sequence of actions. We’ve taught our robot the *concept* of logging in.

Now, look at how clean our actual test file becomes. It doesn’t need to know anything about input fields or buttons. It just needs to know that a LoginPage exists and that it has a login method.

const { Builder, until } = require('selenium-webdriver');
const LoginPage = require('../pages/LoginPage');

describe('Login Functionality with Page Objects', () => {
  let driver;
  let loginPage;

  beforeAll(async () => {
    driver = await new Builder().forBrowser('firefox').build();
    loginPage = new LoginPage(driver);
  });

  afterAll(async () => {
    await driver.quit();
  });

  test('should log in a user successfully', async () => {
    await loginPage.login('testuser', 'password');

    // Assert we landed on the right page
    await driver.wait(until.titleIs('Dashboard'), 2000);
    const url = await driver.getCurrentUrl();
    expect(url).toContain('/dashboard');
  });
});

The benefits here are enormous. First, maintainability. If the login form changes, you only have to update the LoginPage.js file. All your tests that use it will continue to work without modification. Second, readability. The test now reads like a specification of what the user does: loginPage.login(...). It’s crystal clear what the intent of the test is. This makes your test suite an actual form of living documentation for your application.

By abstracting away the implementation details of your pages, you can build a robust and scalable test suite. You’re no longer writing brittle scripts; you’re building a library of interactions that model how a real user navigates your site. This is the critical step toward creating tests that you can actually rely on. The next step is to handle more complex UI components, like dynamic tables or custom dropdown menus, which often require more sophisticated waiting strategies than a simple until.titleIs. For example, you might need to wait for a specific network request to complete or for an element to have a certain CSS class applied after an animation.

The part where you actually start trusting your code

So your Page Objects are looking good, but your tests are still flaky. They pass on your machine, but fail intermittently in the CI pipeline. You start throwing driver.sleep(5000) in there, hoping to give the page “more time to load.” Stop. Right. Now. Using fixed sleeps is the testing equivalent of putting duct tape on a leaky pipe. It might hold for a bit, but it’s going to burst, probably during a late-night deployment. The problem is that modern web apps are asynchronous beasts. Content doesn’t just appear; it loads, it renders, it animates. You aren’t waiting for a fixed amount of time; you’re waiting for a specific state.

This is where Selenium’s explicit waits come in. You’ve already seen a simple one: driver.wait(until.titleIs('Dashboard'), 2000). This tells the driver to poll the DOM for up to 2 seconds until the condition (the title being ‘Dashboard’) is met. If it’s met in 100ms, the test continues immediately. If it’s not met after 2 seconds, it throws a timeout error. This is infinitely better than a blind sleep(). The until module provides a bunch of useful pre-built conditions, like elementLocated, elementIsVisible, and stalenessOf.

Let’s say you have a dashboard page that loads a list of projects from an API. Clicking a “Load Projects” button triggers the API call, and a loading spinner is shown. When the data arrives, the spinner disappears and the project list is rendered. How do you test this? You need to wait for the spinner to disappear and for the project list to appear*. Here’s how you might implement that in your DashboardPage object.

const { By, until } = require('selenium-webdriver');

class DashboardPage {
  constructor(driver) {
    this.driver = driver;
    this.projectListContainer = By.id('project-list');
    this.loadingSpinner = By.css('.spinner');
    this.projectItems = By.css('.project-item');
  }

  async waitForProjectsToLoad() {
    // First, wait for the spinner to go away.
    // This is crucial for apps that remove elements from the DOM.
    const spinner = await this.driver.findElement(this.loadingSpinner);
    await this.driver.wait(until.stalenessOf(spinner), 5000);

    // Now, wait for the project list container to be visible.
    const projectList = await this.driver.findElement(this.projectListContainer);
    await this.driver.wait(until.elementIsVisible(projectList), 5000);
  }

  async getProjectCount() {
    await this.waitForProjectsToLoad();
    const projects = await this.driver.findElements(this.projectItems);
    return projects.length;
  }
}

module.exports = DashboardPage;

In our test, we can now call dashboardPage.getProjectCount() and be confident that the test will wait intelligently for the page to be ready before it tries to count the elements. The test becomes robust and reliable. It’s no longer a race against the network.

// In your test file...
test('should load projects on the dashboard', async () => {
  // Assume we are already logged in and on the dashboard
  const dashboardPage = new DashboardPage(driver);
  const projectCount = await dashboardPage.getProjectCount();
  expect(projectCount).toBeGreaterThan(0);
});

But what if the built-in conditions aren’t enough? What if you need to wait for an element to have a specific text, or for a list to contain at least five items? You can write your own custom wait conditions. A condition is just a function that takes the driver as an argument and returns a truthy value when the condition is met, or a falsy value otherwise. Selenium will call this function repeatedly until it returns true or the timeout expires.

Let’s write a custom condition to wait until the number of .project-item elements is greater than a specific count. This is useful for verifying that paginated data has loaded correctly.

const { By } = require('selenium-webdriver');

function numberOfElementsToBe(locator, expectedCount) {
  return async function(driver) {
    const elements = await driver.findElements(locator);
    return elements.length === expectedCount ? elements : null;
  };
}

// How you'd use it in a test:
test('should load exactly 10 projects on the first page', async () => {
  const dashboardPage = new DashboardPage(driver);
  await dashboardPage.waitForProjectsToLoad();
  
  const projectItemsLocator = By.css('.project-item');
  await driver.wait(numberOfElementsToBe(projectItemsLocator, 10), 5000);

  // The test passes if the condition is met within 5 seconds.
  // No need for a separate assertion if the wait itself is the check.
});

This is the real turning point. When you master intelligent waits and structure your tests with Page Objects, you move from writing flaky scripts to engineering a reliable, automated quality assurance system. Your tests stop being a source of frustration and become a safety net that catches bugs before they escape into the wild. You can deploy with confidence, knowing that your robot army has meticulously checked every critical user path. This is when you actually start trusting your code, because you’ve built a system to verify it.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *