Adding Cypress to an existing Angular project
Tags: angular, cypress, testing, typescript
Published: 2021-12-29 02:14:54

(Originally published on my blog December 23, 2021)

When I set up my Angular13 single-page application (SPA) at work, I initially considered setting up Selenium for end to end testing, but due to the steep learning curve I was experiencing with the stack in general (and some other projected hurdles such as figuring out how to mock data that would be accessed through the `window.external` property in the live environment), I kept putting it off.

But, in the past month I encountered a situation in which the tests that I have written for this project did not actually catch a dependency error that caused the page not to load when the template was accessed at runtime.  I suspect this is because I am *not* using the `TestBed` for the majority of my unit tests, since TestBed has a high overhead and slows down my couple thousand test cases dramatically.

I also recently read some articles (particularly [From Zero to Tests](https://corgibytes.com/blog/2019/12/16/From-Zero-to-Tests/) on Corgibytes) that got me rethinking my test structure. Most of my tests currently are simply unit tests, possibly a few integration tests - mostly, does this function do what we expect when given the inputs we expect it to get (or handle it gracefully if we give it bad inputs)?  I definitely don't have any tests that address DOM rendering. So, I had a stretch of downtime where I don't have anyone waiting on any specific projects and figured it would be a great time to set something up.

---

> **Why Cypress?**
> You may have noticed that I mentioned Selenium to begin with, but this article is about Cypress. Going back to Corgibytes, this time to [Integration Tests Can Be Fun!](https://corgibytes.com/blog/2017/02/21/integration-tests-fun/), I didn't really care for the idea of setting up a black box that was hard to understand to handle my higher level tests. Plus, I've heard good things about Cypress from other sources, and it looks like it is relatively easy to set up, compared to Selenium.

---

## Getting Started

The first thing I did after deciding to set up Cypress was hop over to [their website](https://docs.cypress.io/guides/overview/why-cypress) and start watching the [introduction video](https://www.youtube.com/watch?v=LcGHiFnBh3Y). This video suggests to install Cypress in any project with `npm install -D cypress`. I'm sure this would work, but I am also aware that Angular has its own package addition process, and sure enough, double checking for an Angular-specific install path lead me to [End-to-End testing with Cypress - Testing Angular](https://testing-angular.com/end-to-end-testing/), which gives the Angular CLI command `ng add @cypress/schematic` instead.

---

> **NOTE:**
> If, like me, the first thing you do after installing Cypress is run it with `npm run cypress:open`, you may be confused by the immediate error message of "Warning: Cypress could not verify that this server is running." Don't worry about this for now if you're following the tutorials. Later in, they go over running your development server alongside Cypress for testing.

---

From there I hopped into the [introductory tutorials that Cypress provides](https://docs.cypress.io/guides/getting-started/writing-your-first-test) to get started. This is my first experience with a browser automation suite, and I have to say, it's pretty fun to enter commands in text, hop back over to the browser window and see it executing them! Definitely excited to push forward and get to a state where I could test my actual application.

So, the next step was to ensure that Cypress actually could test against it. The Cypress tutorials direct the user to enter a localhost address into `cy.visit()`, but the Angular CLI `ng add` command we used earlier already set up a default localhost address that's *different* from the one mentioned in Cypress's tutorials... *and* matches the one available from `ng serve`. If you have customized `ng serve`, I don't know if this will hold true, but in my case the address Cypress was looking for is a match.  (If you want to customize the localhost address, the Cypress tutorial goes over configuring this in [Step 3 of Testing Your App](https://docs.cypress.io/guides/getting-started/testing-your-app#Step-3-Configure-Cypress).)

Angular also provides a basic test file with a couple of `contains()` assertions when you use `ng add` to include Cypress in your project. In my case, since this is an existing project with actual content, neither passed. I'm not sure whether they're valid for the empty new app that Angular's CLI generates when creating a new application; I suspect they might be.

My case is a bit unusual in that a user should never actually see this page; users access individual pages by URLs that are stored in the software that they show up in. The only time a user would end up at the root of the application is if somehow the stored link in the software didn't match any valid route. Nonetheless, it's useful to ensure that the page loads and renders, since if it doesn't, none of the rest of the application will either! I ended up just writing a few basic test cases against the minimal content and links that I have at the root of my application for proof of concept.

I also took one of the user-facing pages that was fairly simple and worked up an initial-state test for that, too, as proof of concept. I added `data-cy="whatever"` tags to the important elements, and had Cypress check that the ones that should exist on page load, do; and the ones that should be hidden on page load, are.

## Setting up the Mock Environment

Most developers at this stage are going to be starting to test their login flow and look at building stubs for server requests. For my application, though, the software that the pages get loaded in handles all of that, my code doesn't have anything to do with authenticating a user. So what I needed to do next was figure out how to stub the entire interface that lives on `window.external` when the pages are live in the production environment.

### What Didn't Work

First I decided to try using the custom commands feature to add a command to apply the mock I've been using for my unit tests to `window.external`. There was an immediate hurdle involving Typescript and parsing the commands file: as soon as I copied the example namespace declaration from the comment at the top of `commands.ts` and adapted it to name my custom command, I got `Argument of type 'mockWindow' is not assignable to parameter of type 'keyof Chainable<any>'.ts(2345)` from my linter. I tried a few things and got other errors, then ended up following along with [Adding Custom Commands to Cypress Typescript](https://medium.com/@gghalistan/adding-custom-commands-to-cypress-typescript-28d23f90c2fd). I created an `index.d.ts` file in `./support` to hold my modified namespace declaration, and added `./support` to the `tsconfig.json` in the `cypress` folder, which resolved the type errors.

```typescript
// index.d.ts
declare namespace Cypress {
  interface Chainable {
    mockExternal(): Chainable<Element>
  }
}
```

Unfortunately, then the part of the custom command where I import the mock caused a huge error, the first part of which looked like this:

```javascript
Error: Webpack Compilation Error
./node_modules/@angular/router/fesm2015/router.mjs
Module not found: Error: Can't resolve '@angular/common' in 'C:\Users\adunster\Documents\repos\HtmlApp\node_modules\@angular\router\fesm2015'
resolve '@angular/common' in 'C:\Users\adunster\Documents\repos\HtmlApp\node_modules\@angular\router\fesm2015'
  Parsed request is a module
  using description file: C:\Users\adunster\Documents\repos\HtmlApp\node_modules\@angular\router\package.json (relative path: ./fesm2015)
    Field 'browser' doesn't contain a valid alias configuration
    resolve as module
// ... 
```

I tried getting around this by duplicating my mock to a file in cypress's directory and editing out any references that imported anything in my Angular project such as types or mocks. Only then I did I find out that nothing I assigned to a property of `window.external` was actually showing up in the browser.

### How did I fix it?

My ultimate solution was to skip using Cypress commands entirely, and change program behavior based on environment variables. I'll probably refine this to a more specific npm script, maybe with an additional `environment.ts` file (as opposed the development environment in general) in the future. For now, the code that assigns the `.external` property in my Angular service changed from:

```typescript
function _external (): any {
  return window.external
}
```

To:

```typescript
function _external (): any {
  if (environment.production) {
    return window.external
  } else {
    return new MockCEMRWindow().external
  }
}
```

Now, as I develop my specific Cypress tests, I can mock data into the functions in the `MockCEMRWindow.external` property to produce the results I need to test. It's not ideal, but for the moment it will do what I need it to do, and actually is something I'd meant to do for a while for manual testing in a browser window running the development environment. I'm sure I will want to dig further into `cy.stub()` and `cy.spy()` when I get the chance, too.

## Running the Tests Automatically

I'm sure there's a lot of room for growth in automating our deployment processes, but for now, the way building and deploying to production is currently set up is through the npm scripts in the root `package.json`. After completing code and testing on a feature or bug fix, the code gets merged into the `fresh` branch, and then we run an npm script that will cause the production server to pull the `fresh` branch, run the Karma/Jasmine unit tests, and then run build it if the tests don't fail. (It's similar for the non-production server we use for semi-live testing on real data, only usually on the current development branch instead of `fresh`.)

Unfortunately, it looks like this will be neither simple nor straightforward to set up for this situation (especially given that all of these have to live inside a `pushd` command since we are using UNC paths), so this will be an adventure for another week. But Cypress has some direction here in their [Continuous Integration](https://docs.cypress.io/guides/continuous-integration/introduction#Boot-your-server) documentation if you're looking for where to head next.

## Conclusion

After getting my feet wet with Cypress and seeing a few of the things it can do, I'm actually really genuinely excited about it. It looks like a great tool and I can't wait to use it to help guarantee the resiliency of my code!

It isn't too hard to get the basics set up on an existing project, although there are a few tricks you may have to deal with depending on your environment. Their tutorials are great, though, and their documentation is clear and easy to follow.

I think if I were building a page that works through more typical API calls and HTTP requests it would be a lot easier to set up appropriate mocks and stubs, or at least have a lot more clear direction. Still, even with the unique challenges of my environment, the basic setup has not been difficult at all. Don't be afraid to just install it and get started!