Skip to content
Brett Zeidler

AUTHOR

Brett Zeidler

Senior Software Engineer

Senior Software Engineer whose main experience was building and leading native mobile iOS applications, but is now looking to do the same in the web front-end world.

Select...
Select...
Utilizing Cypress Testing in a Multi-App Monorepo cover image

Utilizing Cypress Testing in a Multi-App Monorepo

For web developers, Cypress is a pretty well-understood testing library that everyone has at least come across or heard of. Getting it set up for an app is pretty straightforward, and you can be off and writing tests in a matter of minutes. But what if you have a monorepo with multiple apps? Do you set up a per-app test suite and manage multiple sets of code in multiple places? Or have you already set that up and noticed that there's a lot of redundancy with potentially shared code that you'd like to refactor into one place? I was recently tasked with setting up such a Cypress testing structure in our Showcase section for our starter.dev project. The idea of the Showcases is that we utilized each of our framework packages to create a GitHub clone as an advanced example of an implementation of each. So they all have the same exact UI to the user, but underneath the hood, they all utilize different sets of technologies for the JavaScript framework, GraphQL/Rest, or CSS libraries. I instantly figured that there had to be a way to write one set of tests that could be utilized against each app, and all I had to do was unify the data-testid attributes across all of the apps. But how do you set it up to automate the process, and against so many different apps? Would it even be possible to start, test, and stop each app through a script? Thankfully, the answer is yes -- and this blog will document and explain a structure that I used when solving that problem. Prerequisites If you've already been developing in a monorepo and have everything set up for that, you likely already have all requirements necessary to install Cypress. However, if you're not and you're setting everything up from scratch, the Cypress docs list a few system requirements: * macOS 10.9 and above (64-bit only) * Linux Ubuntu 12.04 and above, Fedora 21 and Debian 8 (64-bit only) * Windows 7 and above (64-bit only) If you're on a Linux distrobution, pay special attention to the dependencies you'll be needing as well. If you're using npm, you'll need: * Node.js 12 or 14 and above It's possible to download Cypress directly, but I don't recommend that approach for the purposes of this guide. Project Structure For this example, I will be showing the structure I used in the starter.dev GitHub Showcases repository. But hopefully it demonstrates that it's flexible enough to be used on any monorepo structure with any number of apps. The folder structure will look something like this when we're done (showing just two apps and the relevant folders/files for succinctness): ` Installation In the root of your project, you'll want to make your directory where your Cypress tests will exist (replace tests-e2e with whatever you'd like your folder to be called): ` Then, once inside this folder, install Cypress via npm: ` Or via yarn: ` Next, we'll install start-server-and-test, which will be needed later on to automate starting our apps and running our test suite against them. Let's also install TypeScript: ` Or via yarn: ` In the newly created package.json in this folder, let's add a basic script to open Cypress: ` Configuration Now that we have everything installed, we can start configuring Cypress. Let's open Cypress via the newly created script in our last step, npm run cypress:open or yarn run cypress:open. Once Cypress opens, select the E2E Testing configuration and click continue at the bottom of the list to create all the default files (make sure to read what each one does if this is your first time using Cypress!). You should see in your folder structure that Cypress created a number of files and folders automatically, but let's create a few additional folders: ` Inside cypress/configs let's create a file called app1.config.js, or app1.json if you're using a Cypress version older than 10 (replace app1 with the name of one of the apps you want to test against): ` Just regular JSON format for versions prior to Cypress 10: ` A couple notes, baseUrl is the URL your app will deploy on when started up, startCommand is the commmand your app uses to start (for me, it was different for a few different apps, but if yours all use the same command you may not need this), and integrationFolder is where all the test .spec files will be. This can be customized if you've already decided that you'd like your tests to be written separately and/or only one of them will need unique tests, etc. But we can leave it alone for now. Additionally, Cypress has quite a few configuration options. But, the one I'd like to point out specifically is the env option. Just like a .env file, you can utilize this to pass in specific parameters or options into your Cypress tests. Specifically, the different apps I was getting this test suite working against handled auth differently in a few cases, so I needed to visit a specific URL to fire off a redirect/auth chain to mock its state. It looks like this (will be the same in any version of Cypress): ` This may not be something you specifically need yourself, but you can pass in whatever you need for your specific apps, and it will only inject into the Cypress state for that app's Cypress config file. So, maybe you could use this to get the name of what app you're testing, etc. Next, let's add a basic test file inside cypress/e2e or cypress/integration if under Cypress 10. Call it whatever you'd like, but I'll name it first-test.cy.ts (or first-test.spec.ts if you're using a Cypress version older than 10): ` Lastly, let's set up a few run scripts to automate running against all our apps. Update the scripts section of your package.json as such: ` For Cypress versions prior to 10: ` Tying it all together If your entire project structure is set up properly and the file names and configurations all match names properly as described at the start of this blog, the scripts should work as is. The usage would looks like this: ` ` The first command, test, runs through all of the test specs that your config file you set up points to in the integrationFolder option in the command line. This option is good to quickly verify passing tests in the background and/or on your CI/CD pipeline. It first fetches the config file from cypress/config/, grabs the baseUrl from that config file, utilizes start-server-and-test to start your target app, and once it's running, it will run your test suite and tear everything down. This is a powerful and flexible option to then chain together running all your Cypress test suites for all your apps back to back from the same place. The next option, test:watch is the option you'll want to use when developing tests. All it does is open Cypress against the target app's config file you've set up. Then in another process, you will still manually need to start your app locally. The benefit of this is once you change code in either the app or your Cypress test spec files, both will update automatically while everything is still open. Conclusion The solution laid out here isn't one for every single monorepo. However, I believe it can eliminate redundancy for certain types of monorepo structures where each app inside is similar enough, or even more rarely, each app _is_ the same but the target deployments or underlying technology is different. Instead of a per-app Cypress installation and test suite in each app package, this may be exactly the solution to abstract or even refactor them to one single place. There's even flexibility built into this structure to allow a unique set of tests against only one or some of the apps you will need to have integration tests for. Of course, this is only the first step. But hopefully it eliminates potentially the most problematic one. If you'd like to read further on writing Cypress tests themselves, we also have a great guide on writing tests themselves with Cypress that you can check out....

How to Use Custom Domain with Serverless: The Perfbuddy API Use Case cover image

How to Use Custom Domain with Serverless: The Perfbuddy API Use Case

Recently, Perfbuddy has transitioned its backend stack to utilize Serverless when managing the AWS Amplify backend, in favor of using pure Amplify. I was brought on near the end of that process to lend a hand, after previously wrapping up migrating another project's backend over to the Serverless framework. Out of the tasks I helped with, one of the bigger challenges came from setting up custom domains for our API endpoints that the frontend would point to. That is, instead of a difficult-to-keep-track-of URL generated by AWS, we can use a custom domain such as api.perfbuddy.com that points to an API Gateway domain where the API routes are set up. Much more developer friendly to remember, _especially_ if you have multiple environments set up for various stages. Fortunately, Serverless has a plugin called Domain Manager that can handle all of that for us. The potential problem? The site had already deployed in a production state inside of another AWS account, and its domain was registered to that account -- separately from where the Serverless backend was being set up. There is a process to migrate that, but having never done it before and the site needing to stay live, I decided to see if I could make it work as it was. Fortunately, I was able to. Prerequisite Before we take a deeper look, if you're following along looking to solve a similar problem and you haven't yet started your Serverless migration, we've discussed that process previously in detail, and you'll definitely want to have the basics already set up for your Serverless deployment instead of doing this all in one go. If you're already deploying with Serverless, you probably also have an IAM role created and set up with the permissions you'll be needing. Just to be sure, you'll be needing these permissions: ` The Problem As I stated previously, the Perfbuddy domain was registered in a separate AWS account than the one our new Serverless deployment was in under our organization account. Fortunately, I had access to both accounts so either a) I could make this work or b) I would have to migrate the domain over and be a nervous wreck the whole time that I'd break something. My first step was just to naively follow the excellent Serverless Domain Manager plugin guide, which got me most of the way there. I added the plugin to the project: ` Then, added the plugin to the the serverless.yml: ` Note: You'll want to make sure you pay attention to the order of your plugins, since for some plugins, the order matters greatly. If you've been paying attention to the order of your plugins already, you probably know whether or not if it's safe to list it first, last, etc. For my purposes, I added it to the end of the list without issue. Next, I needed to add the plugin configuration under the custom field: ` The documentation specifies multiple ways to set up multiple domains in one configuration setup, but for my first attempt, I decided to try to get just one domain setup to see if it worked (more on setting up multiple domains later). I left out a few of the parameters that were listed in the guide, and only added a few that I knew I needed to specify. Most parameters have a default value if not specified, which you can find on the plugin's Github. After running the create command: ` I hit my first real snag: there was no hosted zone set up for the custom domain. The Solution At this step, I feared I would actually potentially have to migrate the domain over. However, when reading the migration documentation, I noticed a specific line in the second optional step that would clue me in on exactly what I needed to know. It reads as follows: > If you're using Route 53 as the DNS service for the domain, Route 53 doesn't transfer the hosted zone when you transfer a domain to a different AWS account. If domain registration is associated with one account and the corresponding hosted zone is associated with another account, neither domain registration nor DNS functionality is affected. The only effect is that you'll need to sign into the Route 53 console using one account to see the domain, and sign in using the other account to see the hosted zone. So, since I had access to both accounts under the same organization, it read to me that AWS would handle the DNS automatically without transferring anything. A hosted zone can live on one account while the main domain registration lived on the other. Perfect. Navigating to the Route 53 console where the Serverless backend was being deployed, I created a hosted zone under the exact domain name I wanted to deploy to (I also created hosted zones for the other specific domains I would need): At this point, the Serverless CLI may have worked with the configuration we specified as is, but I decided to add an additional parameter using the hosted zone ID from the one I just created: ` Now, running the create_domain worked and navigating to the API Gateway console showed the custom domain we just created: Additionally, navigating back to our hosted zone in the Route 53 console, we created shows two additional records the plugin created for us. Finally, running a deploy command: ` Successfully deployed the backend, now with the domain manager plugin info appearing in the output: ` Deploying Multiple Domains Of course, it wasn't just a single API backend I needed to setup; I needed to setup multiple. The problem I saw with using the plugin's built-in capability for multiple domains was that it would try to deploy to all of them at the same time. We needed to deploy to different stages, at different points of development and release independently between them. So, utilizing the Serverless stage parameters I was able to use the same single custom domain structure for the domain manager plugin configuration. You can even specify whole arrays as a parameter, so I did exactly that (with the other stages and parameters added as needed, of course): ` Note: Be sure to take note that you are using the correct YAML formatting and structure for you use case -- at one point, I was using the multiple domains YAML structure, but incorrectly so my deployments weren't actually going through as a result, I believe. I spent far more time resolving that specifically than I needed to. Then, back under the actual plugin configuration, I changed it as follows: ` Now, running any of the commands: ` Deployed to all needed environments separately, and pointing my locally running frontend to api.perfbuddy.com worked like a charm: Conclusion Working in Serverless truly has been a smooth process, and we're constantly blown away with its power that can only succinctly be described as magic. And, of course, if you haven't yet checked out Perfbuddy, be sure to do so! It's completely free and was made to help all developers and their teams improve their products....

Getting Started with Angular: A Mobile Developer’s Approach cover image

Getting Started with Angular: A Mobile Developer’s Approach

At many points in your career, you will find yourself picking up a new framework, platform, language, or (most likely) some combination of all three. In my case, I recently found myself looking for a change of pace, and ended up switching from working in native iOS as a Mobile Engineer to the world of JavaScript as a Front-End Engineer. Specifically, I started out with Angular. No matter how you slice it, learning something brand new can feel daunting — regardless of whether it is mid-career or otherwise. Luckily, Angular is extremely simple and quick to get set up . . . but getting an environment and project set up really is only a small part of the path to learning a platform, isn’t it? As I challenged myself to attempt to learn Angular as quickly as possible, I aimed to first break apart the approach by understanding: * What Angular is * What Angular's goal is * What the fundamental pieces of Angular are So, before we jump straight into some code, let's grasp these concepts first. What *is* Angular? First of all, Angular is not AngularJS. This was slightly confusing for me at first, and I made the mistake of mixing them up initially. Oops. At a macro level, Angular is a modern JavaScript framework that’s built on TypeScript, giving it access to static types, classes, and other elements provided by ES6. This allows code to organize and structure properly so it can be Object-Oriented, and allow for long-term maintenance at any scale, which will be important for the next fundamental concepts. What does Angular aim to achieve? Angular provides a platform to make single-page applications. I think it's important to point out the usage of the word platform, as Angular comes with quite a bit more to help us scale applications to any point we need. It ships with many integrated libraries (more on that in a bit), integration with numerous third-party libraries, and, maybe most importantly, the Angular CLI (command line interface), which gives us access to build, serve, test, and even ship our application. "Application" is also a very important distinction- that is, Angular (like many modern frameworks) aims to provide the tools to create responsive, smooth user experiences. What are the essential building blocks of Angular? To understand Angular, you have to understand what a Component is. But first, it may be easier to first visualize what makes an application: navbars, buttons, tables, table cells, and every other separately distinct building block you can visually see. In Angular, every single one of these is a Component, and everything else that exists is in service of these Components. For me, this is where it really started to click, since in iOS, the same fundamental principle exists: every single object on the screen *must* conform to UIView, including any View Controller. Angular, of course, is no different — the root view of the app itself is also a Component. Templates define the appearance of the Component via HTML. Angular extends the functionality of templates quite a bit through direct text interpolation, directives, and property/event binding. These, combined with CSS, can achieve essentially anything you need it to without having to entangle your Component's business logic with the presentation of said Component. The final core piece of the Angular paradigm is dependency injection. Essentially, dependency injection helps keep your Components extremely light, modular, and maintainable. So, say you have a table that needs data from a request sent to an API endpoint. You *could* just have the inner workings of a server fetch right there all in the component, but you likely have multiple places all over your application where you'll be making API requests, and have to duplicate that code. Instead, Angular lets you define a Service object that can handle that API request, and return the data directly to your component. You don't even need to instantiate the Service since Angular will just automatically do that for you once you inject the dependency to keep things even cleaner. Dependency injection, services, routing, etc. can feel daunting, but I think as long as you view everything else that isn't a Component solely existing to work *in favor* of Components, things become a lot clearer and you can start building apps the Angular way. Speaking of, let's do a bit of project setup, take a look at the structure, build it, and add a component. Prerequisties Now that we know what Angular is, we can finally start our first project. A couple things we will need first: * Visual Studio Code * NodeJS and NPM with at least a basic proficiency with a terminal * Some basic understanding of HTML and JavaScript/TypeScript Creating and running your first project With Node and NPM installed, we can install the Angular CLI with the following command: ` Then we create our first project with: ` Note: The above command will have a couple prompts for you. Answer yes to add Angular routing, and the style sheet format is up to you (I chose SCSS). You can just press enter on both prompts for the default setup as well. And finally, building and serving our new app locally: ` Open http://localhost:4200/ on your favorite browser to see what we just made. One note about ng serve is its ability to pick up code changes and update live — no waiting for build times just to see very small changes here. Nice. Project Structure Of course, it's helpful to know what the CLI just built out for us. Looking at our project structure in VS Code, we can see quite a bit of config files set up in the root directory. Of course, these are all important, but for now, let's focus on getting familiar with a few: * angular.json — This is the configuration file for CLI, so it knows how to build, serve, and test your app. Should be good to go right out of the box. * package.json — The file where the list of all your NPM dependencies live, the package-lock.json specifies the versions for these files (for my iOS friends, this is very similar to the podfile or build.gradle for my Android friends!). Whenever you install a Node package, the tool will automatically update these for you. * node_modules — With all the above dependencies installed, the .gitignore should be ignoring this directory by default, but it's good to know everything is there. Now that we're familiar with the housekeeping of the app, we can dig into the most important part: the code. In the src/app directory, we can see our first module, component, and the files that make it up. Let's ignore those files for now, and create our very first Component. Creating The First Component Within your terminal, we'll run the following command: ` With that, CLI should have made a directory in src/app/my-first-component and the following files: 1. my-first-component.component.ts — This is the file where the data and logic needed to update your Component's Template live. 2. my-first-component.component.html — The associated Template for the Component, where the HTML is defined to actually render our Component. 3. my-first-component.component.scss — The associated style sheet for the Template, where styles scoped only to this Component are defined. 4. my-first-component.component.spec.ts — The very first unit test for the Component, automatically generated by CLI. How nice! Within the component.ts file, let's take special note of the following: ` Here we can note the Template and style sheet URLs, but of particular note is the selector, in our case called app-my-first-component. This is the custom HTML tag we can now use anywhere our Component is imported in our project. The reason app is prepended to the front of the name is because we defined this Component within the app module, but you can absolutely change the name if you want. If we look back to the root app directory in src/app (taking special note of the exact same file structure we just created in my-first-component), let's open app.module.ts. The CLI has already created a declaration for our Component in the NgModule decorator, and imported MyFirstComponentComponent automatically for us. Let's actually get our newly created Component on the screen somewhere. Let's open app.component.html. There's quite a bit of HTML already in here, but we can leave it mostly alone for now. Really, we can throw our Component just about anywhere, but let's search for the following: ` And replace it with: ` Save your changes, and check your browser to see the following: Now that we've got it up on the screen, let's do some slight modification to get data from the logic to the Template. First, add the following inside the class definition of my-first-component.component.ts: ` Then, inside my-first-component.component.html, replace the Template definition with the following: ` The double bracket notation tells Angular to interpolate the value within, and dynamically pull the text defined in our class. Save both files, and you should see the following: Neat, right? Conclusion Whenever someone asks me how easy it is to get started doing native iOS app development, it's easy for me to boast that you can start a project, throw something in Interface Builder, and run it — all within a few minutes' time. Now, I (and hopefully you) can easily say the exact same about Angular. Of course, this is just a very small taste of what Angular has to offer, but hopefully instead of feeling initially lost in a sea of documentation, the relative simplicity of the platform will seem a lot more clear from the essence of what I described here from my journey of also having to learn Angular. From here, I think it's valuable to look through the incredible official Angular docs, and I'd recommend running through some of their tutorials as well....