<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Testing on blog.iankulin.com</title><link>https://blog.iankulin.com/tags/testing/</link><description>Recent content in Testing on blog.iankulin.com</description><generator>Hugo</generator><language>en-AU</language><lastBuildDate>Mon, 12 May 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.iankulin.com/tags/testing/index.xml" rel="self" type="application/rss+xml"/><item><title>End to end testing - Cypress basics</title><link>https://blog.iankulin.com/end-to-end-testing-cypress-basics/</link><pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/end-to-end-testing-cypress-basics/</guid><description>&lt;p&gt;When you&amp;rsquo;ve made a change to your web-app, do you run it then click around the new bits to check it works? Good start, but instead of doing that yourself, do it in a faster, more comprehensive and automated way with an end-to-end (E2E) testing setup using &lt;a href="https://www.cypress.io/"&gt;Cypress&lt;/a&gt;. Here&amp;rsquo;s how.&lt;/p&gt;
&lt;h3 id="e2e"&gt;E2E&lt;/h3&gt;
&lt;p&gt;End to End testing is testing your app as a user might - by clicking links, entering data, looking at the screen and checking everything is okay, but it&amp;rsquo;s scripted like a unit test and the results are checked with assertions. Like unit testing this allows you to build up a collection of comprehensive tests that easily detect for unexpected behaviours - not just in the results of functions in your app, but in the user experience of the app.&lt;/p&gt;
&lt;p&gt;In the case of Cypress, this works by running your app in an instrumented browser. The tests are written in JavaScript and might ask things like &amp;ldquo;Click the &amp;lsquo;Home&amp;rsquo; link&amp;rdquo; and have an assertion similar to &amp;ldquo;check the home page loaded&amp;rdquo;. Let&amp;rsquo;s see how that will look.&lt;/p&gt;
&lt;h3 id="how-it-looks"&gt;How it looks&lt;/h3&gt;
&lt;p&gt;In the app I&amp;rsquo;m working on, if you view an individual customer (say at &amp;ldquo;&lt;a href="http://127.0.0.1:3002/customers/1"&gt;http://127.0.0.1:3002/customers/1&lt;/a&gt;&amp;rdquo;) there&amp;rsquo;s a &amp;ldquo;Home&amp;rdquo; link at the top which takes you to the list of customers (at &amp;ldquo;&lt;a href="http://127.0.0.1:3002/customers"&gt;http://127.0.0.1:3002/customers&lt;/a&gt;&amp;rdquo;).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-10.22.34.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the test code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;describe(&amp;#39;Page Navigation&amp;#39;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#39;should navigate to the customers list when clicking the Home link&amp;#39;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // visit the customer details page
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#39;http://127.0.0.1:3002/customers/1&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // find and click the Home link
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#39;a&amp;#39;).contains(&amp;#39;Home&amp;#39;).click();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // verify we navigated to the customers list page
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#39;eq&amp;#39;, &amp;#39;http://127.0.0.1:3002/customers&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you&amp;rsquo;ve been writing unit tests before, this format will be familiar, but let&amp;rsquo;s look at the steps:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.visit(&amp;#39;http://127.0.0.1:3002/customers/1&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You guessed it - we&amp;rsquo;re telling Cypress to visit that page.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;a&amp;#39;).contains(&amp;#39;Home&amp;#39;).click();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m not sure if Cypress uses &lt;a href="https://jquery.com/"&gt;JQuery&lt;/a&gt;, or just a JQuery like syntax, either way, what we&amp;rsquo;re doing here is selecting the &amp;lsquo;&amp;lt;a &amp;hellip;&amp;gt;&amp;rsquo; tag. Of course our page probably contains several anchor tags, so we&amp;rsquo;re refining this search to the anchor tag that contains &amp;lsquo;Home&amp;rsquo;. Note that there&amp;rsquo;s an implied assertion here. If there is no &lt;a&gt; link on the page containing &amp;lsquo;Home&amp;rsquo;, this test will fail with an error saying something like &amp;ldquo;Expected to find content: &amp;lsquo;Home&amp;rsquo; within the element: &lt;a&gt; but never did.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Finally the &lt;code&gt;click()&lt;/code&gt; at the end of the statement tells Cypress to click this link.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.url().should(&amp;#39;eq&amp;#39;, &amp;#39;http://127.0.0.1:3002/customers&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before we look at this statement, consider that we haven&amp;rsquo;t told Cypress to wait for a bit for the results of our click() to process - one of the benefits of Cypress is it just figures that out magically.&lt;/p&gt;
&lt;p&gt;This statement is an assertion - the URL &lt;code&gt;should&lt;/code&gt; equal (&lt;code&gt;eq&lt;/code&gt;) the URL we&amp;rsquo;ve provided.&lt;/p&gt;
&lt;p&gt;So that gives us a quick overview of a simple test. Naturally Cypress has a heap more operators and assertion types to help us test our application - basically everything you could think of as user-facing testing. Let&amp;rsquo;s look at a simple demo app then work through the tests we might try for this.&lt;/p&gt;
&lt;h3 id="the-app"&gt;The App&lt;/h3&gt;
&lt;p&gt;This app is a simple demo I wrote for an earlier blog post about using the Express router. We have &lt;em&gt;Customers&lt;/em&gt; and &lt;em&gt;Orders&lt;/em&gt;, a single &lt;em&gt;customer&lt;/em&gt; can have zero-many &lt;em&gt;orders&lt;/em&gt;. The opening page is a list of all customers. Clicking on a customer shows the details for that customer, including a list of their orders. Clicking on an order shows the detail for that order, including a link the customer it belongs to.&lt;/p&gt;
&lt;p&gt;The Customer and Order detail views have delete links, and a deletion of a customer should cascade to delete that customer&amp;rsquo;s orders.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-11.50.32-1.png" width="946" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-11.50.22-1.png" width="999" alt=""&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-10.22.34-2.png" alt=""&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-12-at-13.12.28-1.png" width="861" alt=""&gt;
&lt;h3 id="installing-cypress"&gt;Installing Cypress&lt;/h3&gt;
&lt;p&gt;Installing Cypress is straightforward. The install steps from the docs are &lt;a href="https://docs.cypress.io/app/get-started/install-cypress"&gt;here&lt;/a&gt;, but really it&amp;rsquo;s just starting your Node project (so you&amp;rsquo;ve got a package.json) then &lt;code&gt;npm install cypress --save-dev&lt;/code&gt; to add it as a dev dependency. It&amp;rsquo;s a big download so expect it to take a bit. It includes lodash, some AWS stuff, tldts, day.js, a heap of vue stuff - just, it&amp;rsquo;s a lot of big dependencies. Also since Cypress itself does some cool stuff linking into the browser - that functionality requires some code.&lt;/p&gt;
&lt;h3 id="the-tests"&gt;The Tests&lt;/h3&gt;
&lt;p&gt;Actually - the code in our very simple demo above covers about 70% of the testing I do, and the pattern of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Select a user element&lt;/li&gt;
&lt;li&gt;Click it or add some text&lt;/li&gt;
&lt;li&gt;Select another user element to check that worked&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;comes up again and again. So I&amp;rsquo;m going to try not to repeat myself too much. Most of what&amp;rsquo;s new in the following tests will be extra selectors, and assertions. We won&amp;rsquo;t cover all of them, but rather a smattering to get started with.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // test for customers list page
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; describe(&amp;#34;Customers Page&amp;#34;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;should have the home page redirect to customers page&amp;#34;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#34;http://localhost:3002&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;h1&amp;#34;).contains(&amp;#34;Customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;should display a list of customers&amp;#34;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;li&amp;#34;).should(&amp;#34;have.length.at.least&amp;#34;, 5);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;li&amp;#34;).eq(0).contains(&amp;#34;Alice&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;should have working links to customer details&amp;#34;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // click the first customer (Alice)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Alice Johnson&amp;#34;).click();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers/&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;h2&amp;#34;).contains(&amp;#34;Alice Johnson&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="should"&gt;.should()&lt;/h4&gt;
&lt;p&gt;There&amp;rsquo;s a &lt;a href="https://docs.cypress.io/api/commands/should"&gt;massive list of should() assertions&lt;/a&gt;, and they depend a bit on what you&amp;rsquo;ve chained on to. In the first example we looked at we used &lt;code&gt;&amp;quot;eq&amp;quot;&lt;/code&gt; for equals, in the example directly above we&amp;rsquo;ve used &lt;code&gt;&amp;quot;include&amp;quot;&lt;/code&gt; for a partial match, and &lt;code&gt;&amp;quot;have.length.at.least&amp;quot;&lt;/code&gt; for what it says on the box.&lt;/p&gt;
&lt;p&gt;Another handy thing might be testing for &lt;code&gt;&amp;quot;not.exist&amp;quot;.&lt;/code&gt; In my example app if I want to test deleting a &lt;em&gt;customer&lt;/em&gt;, I can check they exist in the customers list, click delete, then check that they no longer exist in the list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;should delete a customer when delete link is clicked&amp;#34;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // first check the customer exists
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#34;http://localhost:3002/customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Hannah Abbott&amp;#34;).should(&amp;#34;exist&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // visit the customer page and delete
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#34;http://localhost:3002/customers/8&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Delete customer&amp;#34;).click();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // verify the customer is deleted
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#34;include&amp;#34;, &amp;#34;/customers&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#34;a&amp;#34;).contains(&amp;#34;Hannah Abbott&amp;#34;).should(&amp;#34;not.exist&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="get"&gt;.get()&lt;/h4&gt;
&lt;p&gt;We&amp;rsquo;ve already seen selecting an anchor tag with get(&amp;ldquo;a&amp;rdquo;) - this will work for any HTML tag, but of course you&amp;rsquo;ll frequently need more specificity than that. As &lt;a href="https://docs.cypress.io/api/commands/get#__docusaurus_skipToContent_fallback"&gt;described in the docs&lt;/a&gt;, most of the JQuery selectors will also work with get.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Select by element type
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;button&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Select by class
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;.my-class&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Select by ID
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;#my-id&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Combining selectors
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;button.primary#submit&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Select by attribute
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(&amp;#39;[data-test=&amp;#34;submit-button&amp;#34;]&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Those first four are straightforward, but you might not know about attributes.&lt;/p&gt;
&lt;h3 id="attributes"&gt;Attributes&lt;/h3&gt;
&lt;p&gt;As part of the HTML specification, tags can have attributes. You&amp;rsquo;ve been using them all along. For example. this button:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;button id=&amp;#34;submit&amp;#34; class=&amp;#34;btn primary&amp;#34; type=&amp;#34;submit&amp;#34;&amp;gt;Submit&amp;lt;/button&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;has attributes for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id&lt;/li&gt;
&lt;li&gt;class&lt;/li&gt;
&lt;li&gt;type&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These all have particular meanings for HTML, CSS and JavaScript, but actually we can make up our own. For example we could say:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;button type=&amp;#34;submit&amp;#34; data-test=&amp;#34;submit-button&amp;#34;&amp;gt;Submit&amp;lt;/button&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s no specification for &amp;lsquo;data-test&amp;rsquo;, it&amp;rsquo;s just a convention, we could just have easily said:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;button type=&amp;#34;submit&amp;#34; data-green-zebra=&amp;#34;submit-button&amp;#34;&amp;gt;Submit&amp;lt;/button&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that I&amp;rsquo;ve kept the &lt;code&gt;data-&lt;/code&gt; prefix - that is part of the &lt;a href="https://www.w3schools.com/tags/att_global_data.asp"&gt;HTML5 specification&lt;/a&gt;. We could probably make up anything and it would work, but maybe it would conflict with something in a future HTML version, so best stick to &amp;ldquo;data-&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Using attributes for specifying the element we want is highly recommended. Although the element you want to click might currently be the third &lt;a&gt; in a &lt;ul&gt; inside the &lt;nav&gt; - it&amp;rsquo;s easy to imagine it being moved in a future update to your app. Once we&amp;rsquo;ve written an E2E test, we want it to continue to work across future app development, and using a &amp;lsquo;data-test&amp;rsquo; attribute supports this.&lt;/p&gt;
&lt;h3 id="invoke-and-then"&gt;.invoke() and then()&lt;/h3&gt;
&lt;p&gt;Sometimes the exact test you need might not be available, or you need to do some operation as part of your testing that requires a bit more processing. In that case, you can chain the &lt;code&gt;invoke()&lt;/code&gt; method. This allows you to call any jQuery method on an element you&amp;rsquo;ve selected with Cypress, letting you extract specific properties or manipulate the element in ways that aren&amp;rsquo;t covered by Cypress&amp;rsquo;s built-in assertions.&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve got it, you can use &lt;code&gt;then()&lt;/code&gt; to run an arrow function against it to do something. The pseudo codes looks a bit like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy.get(selector)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .invoke(jQueryMethod) // Extract what you need
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .then((result) =&amp;gt; { // Process it with your own logic
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // Custom processing
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Let&amp;rsquo;s look at an example. Imagine the HTML of our page looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;span class=&amp;#34;price&amp;#34;&amp;gt;$24.99&amp;lt;/span&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And we want to check that the price was greater than $20 - perhaps we are supposed to have added tax or something. Our test could look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;.price&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;invoke&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;text&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;priceText&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; priceValue &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; parseFloat&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;priceText&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;replace&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;$&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; expect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;priceValue&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;to&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;be&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;greaterThan&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;20.0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This selects this span based on it&amp;rsquo;s class, then saves the text &amp;lsquo;$24.99&amp;rsquo; to &lt;code&gt;priceText&lt;/code&gt;, extracts the value to a JavaScript number, and asserts it to be greater than 20.&lt;/p&gt;
&lt;p&gt;In my demo app, I use this to check the cascading delete - when we delete the customer, the orders for that customer should also be deleted. Rather than hard code the order number we can use invoke/then to extract it from the text which looks like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;8 - 2025-03-08 - $200&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then, after deleting the customer (and orders) we navigate to the orders page to make sure that order number does not exist there any more.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; describe&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Cascading Deletions&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;should delete owned orders when a customer is deleted&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; first make a note of an order &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; a specific customer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;visit&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;http://localhost:3002/customers/4&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;h2&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;contains&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Diana Prince&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; note an order ID that belongs to this customer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;ul li a&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;first&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;invoke&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;((&lt;/span&gt;orderText&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; extract the full order text to use &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; matching later
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; orderTextFull &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; orderText&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;trim&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; extract just the order ID number
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; orderId &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; orderText&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;split&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)[&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;trim&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; delete the customer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;a&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;contains&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Delete customer&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;click&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; verify customer is deleted
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;url&lt;span style="color:#eceff4"&gt;()&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;should&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;include&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/customers&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;a&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;contains&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Diana Prince&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;should&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;not.exist&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; check that the order is also deleted 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;visit&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;http://localhost:3002/orders&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; make sure we&lt;span style="color:#a3be8c"&gt;&amp;#39;re matching the exact order (not just a substring)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;get&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;a&lt;span style="color:#eceff4"&gt;[&lt;/span&gt;href&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/orders/${orderId}&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;]&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;should&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;not.exist&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There is a lot, lot more to Cypress to this, but with what we&amp;rsquo;ve covered here it&amp;rsquo;s possible to write a comprehensive suite of tests that will test all of the functionality in this demo app in &lt;a href="https://github.com/IanKulin/route-demo/blob/main/cypress/e2e/home.cy.js"&gt;about 200 lines&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="using-cypress"&gt;Using Cypress&lt;/h3&gt;
&lt;p&gt;So that&amp;rsquo;s all the code, but how does it look to test like this? For me this is one of the things that makes end-to-end testing cool. I love seeing it&amp;rsquo;s click away in the browser at super speed as my tests turn green.&lt;/p&gt;
&lt;p&gt;First of all I start my app - so however you normally do this. For me, it&amp;rsquo;s dropping to the terminal in VSCode and running it in Node. Something like &lt;code&gt;node index.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s going we can start Cypress. Since the first terminal is running my node app, we need to spawn another terminal to run Cypress in. This is a simple matter in VSCode - just hit that + button I&amp;rsquo;ve circled in the screen shot below. You can swap between the different terminals you have open by clicking on them in the list underneath that + button.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.02.23.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;We start Cypress in the new terminal with &lt;code&gt;npx cypress open&lt;/code&gt; but the magic does not happen in the terminal, this thing pops up:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.09.06.png" width="900" alt=""&gt;
&lt;p&gt;We&amp;rsquo;re doing E2E testing, so select that.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.09.16.png" width="900" alt=""&gt;
&lt;p&gt;I&amp;rsquo;m in a Chrome mood today, so next we see this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-16.09.29.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve only got one test file - &lt;code&gt;home.cy.js&lt;/code&gt;, so I click that. The tests are listed down the left side of the browser, and my app in an iFrame to the right. As the tests are running, I can see the app flicking through each step. In a couple of seconds the seventeen tests that comprise many page manipulations and assertions are finished and I can see the results.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.14.27.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If we click on a test, the details for it open up, and a screenshot of the application state at the time of that test is displayed.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.16.52.png" width="900" alt=""&gt;
&lt;p&gt;The most common debugging problem I&amp;rsquo;ve run into is when I didn&amp;rsquo;t write the selection correctly (and didn&amp;rsquo;t use a data- attribute). These are easily checked in this view by hovering over the one we&amp;rsquo;re interested in - the element that Cypress used in this step will be highlighted.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.20.34.png" width="900" alt=""&gt;
&lt;p&gt;So, how does a failed test look. I can create that in this test suit just by running the tests again. It won&amp;rsquo;t be able to delete orders or customers it deleted in the earlier run.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-04-15-at-17.24.56.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So, at the start of this &amp;lsquo;delete order&amp;rsquo; test, I&amp;rsquo;m checking if the order exists, and it doesn&amp;rsquo;t (because we didn&amp;rsquo;t reset after deleting it last time). We can see from the error message that Cypress waited 4 seconds in case it was a timing issue. It&amp;rsquo;s displayed the test case where the failure has occurred. This along with the before and after snapshots of the app around each test make locating problems a breeze.&lt;/p&gt;
&lt;h3 id="resets"&gt;Resets&lt;/h3&gt;
&lt;p&gt;The pattern above (where you run a test twice and it fails the second time because the first execution changed the state) is common. To avoid this, we need some system of resetting the state. Cypress has a mocha like &amp;lsquo;beforeEach&amp;rsquo; ability. You most always need this for logging things in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;describe(&amp;#39;My app tests&amp;#39;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; beforeEach(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // This code runs before each test in this block
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.visit(&amp;#39;/login&amp;#39;); // for example
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#39;input[name=username]&amp;#39;).type(&amp;#39;user&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#39;input[name=password]&amp;#39;).type(&amp;#39;password&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#39;button[type=submit]&amp;#39;).click();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#39;should show the dashboard after login&amp;#39;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#39;include&amp;#39;, &amp;#39;/dashboard&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.contains(&amp;#39;Welcome&amp;#39;).should(&amp;#39;be.visible&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#39;should navigate to settings&amp;#39;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.get(&amp;#39;nav&amp;#39;).contains(&amp;#39;Settings&amp;#39;).click();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cy.url().should(&amp;#39;include&amp;#39;, &amp;#39;/settings&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But for apps that need things like a fresh database before testing, it&amp;rsquo;s a bit trickier. In the past I&amp;rsquo;ve sometimes created some sort of /test-reset endpoint which feels like an unreasonable security risk. The proper answer is to shell out with a Cypress task. That way we can do things like copy in test data, or spin up a whole test environment in a container. These are meaty topics for another post - but really, our tests should be re-run-able, and run-able in any order so we might come back to that.&lt;/p&gt;
&lt;h3 id="test"&gt;Test&lt;/h3&gt;
&lt;p&gt;Any testing is better than none, and if you use these sorts of tools that make it easier you&amp;rsquo;ll find you&amp;rsquo;ll add to them, especially when errors crop up. If I sit down to add end-to-end tests to an existing app, I nearly always find things I want to change to make it better. Use end to end testing.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/IanKulin/route-demo"&gt;code&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Node.js built in test runner</title><link>https://blog.iankulin.com/node-js-built-in-test-runner/</link><pubDate>Mon, 17 Mar 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/node-js-built-in-test-runner/</guid><description>&lt;p&gt;For the longest time, I&amp;rsquo;ve been using &lt;a href="https://mochajs.org/"&gt;Mocha&lt;/a&gt; (test runner) and &lt;a href="https://www.chaijs.com/"&gt;Chai&lt;/a&gt; (assertion library) for my JS testing. They are reliable old friends.&lt;/p&gt;
&lt;p&gt;One of the effects of the existence of &lt;a href="https://bun.sh/"&gt;Bun&lt;/a&gt; and &lt;a href="https://deno.com/"&gt;Deno&lt;/a&gt; has been to spur Node onto adding some new features, so after appearing as an experimental feature in 18, the Node test runner dropped in Node 20.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if the familiar unit test layout of Mocha and Node is inherited from Jest, or comes from older testing frameworks of which JUnit and NUnit were the first ones I&amp;rsquo;d ever used. Before that I just used to write tests as lumps of assertions in regular code - which worked but wasn&amp;rsquo;t as pleasant to use as a proper unit test setup. Regardless, the system of bundling a few tests together and having them all run and spit out green ticks is not a new one.&lt;/p&gt;
&lt;p&gt;If you are coming from Mocha, there are very few changes to your practice to make. I didn&amp;rsquo;t read &lt;a href="https://nodejs.org/api/test.html#skipping-tests"&gt;the docs&lt;/a&gt; to check that I had to begin my test files with &amp;rsquo;test&amp;rsquo; or to put them into a &amp;rsquo;test&amp;rsquo; directory. But I discovered that dragging them into a &amp;rsquo;test-dont&amp;rsquo; directory didn&amp;rsquo;t stop them from running.&lt;/p&gt;
&lt;h3 id="testing"&gt;Testing&lt;/h3&gt;
&lt;p&gt;As well as what ever you&amp;rsquo;re testing, you need to import a couple of things from node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import { isEven } from &amp;#34;../index.js&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import { describe, it } from &amp;#34;node:test&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import assert from &amp;#34;node:assert&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then we can write our tests, grouping them in a describe:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;describe(&amp;#34;isEven&amp;#34;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;returns true for even numbers&amp;#34;, (t) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assert.strictEqual(isEven(4), true);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assert.strictEqual(isEven(0), true);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assert.strictEqual(isEven(-2), true);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;returns false for odd numbers&amp;#34;, (t) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assert.strictEqual(isEven(7), false);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assert.strictEqual(isEven(-3), false);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;returns false for most floating point numbers&amp;#34;, (t) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assert.strictEqual(isEven(3.5), false);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // sadly, this is true because JavaScript
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; // assert.strictEqual(isEven(4.0000000000000000000001), false);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; it(&amp;#34;returns false for non-numbers&amp;#34;, (t) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assert.strictEqual(isEven(&amp;#34;a&amp;#34;), false);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assert.strictEqual(isEven(null), false);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assert.strictEqual(isEven(undefined), false);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then to run them, just &lt;code&gt;node --test&lt;/code&gt; and we&amp;rsquo;ll get a nice summary&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-03-07-at-20.15.43.png" width="800" alt=""&gt;
&lt;p&gt;Of course there are a heap of other &lt;a href="https://nodejs.org/api/assert.html"&gt;assertions&lt;/a&gt;, as well as stuff to set-up and tear-down and to &lt;a href="https://nodejs.org/en/learn/test-runner/mocking"&gt;mock things such as times&lt;/a&gt;. What I&amp;rsquo;ve shown here is very much a getting started, but it also deals with about 80% of my testing needs.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not entirely sure how I feel about moving to a built in test runner. The small convenience I gain has to be weighed up against a small amount of lock in to a run time. I haven&amp;rsquo;t yet been tempted to Deno or Bun, but I love that they exist and are spurring on innovation. Possibly I&amp;rsquo;ll continue with portable testing tools for big projects, but for little ones use the built in.&lt;/p&gt;</description></item><item><title>Testing Node.js apps - Mocha, Chai, and Supertest</title><link>https://blog.iankulin.com/testing-node-js-apps-mocha-chai-and-supertest/</link><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/testing-node-js-apps-mocha-chai-and-supertest/</guid><description>&lt;p&gt;Bruno is a great open source Postman/Insomnia replacement, and I&amp;rsquo;ve been using it for basic tests of my node servers using the built in asserts and loving it. This is pretty great, and I gather it&amp;rsquo;s also possible to go beyond this and &lt;a href="https://docs.usebruno.com/testing/introduction.html"&gt;write tests in JS in Bruno&lt;/a&gt;. I believe it also has the hooks needed to build it into your CI/CD systems.&lt;/p&gt;
&lt;p&gt;Any large project is probably going to benefit from a more comprehensive suit of testing tools, and while I&amp;rsquo;ll still be using Bruno, my serious tests will be managed with these other tools.&lt;/p&gt;
&lt;p&gt;I admit I&amp;rsquo;ve probably put this off a bit longer than I should have - I didn&amp;rsquo;t really want to install four dependencies and learn four different things just to test my endpoints. It turns out that using the tools together is seamless, and setting it all up was trivial.&lt;/p&gt;
&lt;p&gt;Speaking of trivial, here&amp;rsquo;s my brilliant Node app. It has two endpoints, both of which do a bit of maths.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; express &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;express&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; app &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; express&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; port &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;use&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;express&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;());&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; endpoint that takes two numbers &lt;span style="color:#81a1c1;font-weight:bold"&gt;and&lt;/span&gt; returns their sumapp&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/sum&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; a&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; b &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; sum&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; a &lt;span style="color:#81a1c1"&gt;+&lt;/span&gt; b &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; endpoint that takes two numbers &lt;span style="color:#81a1c1;font-weight:bold"&gt;and&lt;/span&gt; multiplies themapp&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/multiply&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;req&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; res&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; a&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; b &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; req&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;json&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; product&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; a &lt;span style="color:#81a1c1"&gt;*&lt;/span&gt; b &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt; start the serverapp&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;port&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Maths server is running at http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;localhost&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;port&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="setting-up-your-project-for-testing"&gt;Setting up your project for testing&lt;/h3&gt;
&lt;h4 id="install-the-tools"&gt;Install the tools&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npm install --save-dev mocha chai supertest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;--save-dev&lt;/code&gt; bit installs them as a development dependencies - they will go in your &lt;code&gt;package.json&lt;/code&gt; and everyone who clones the repo will be working with the same version. Additionally, they won&amp;rsquo;t needlessly be installed when deployed to production.&lt;/p&gt;
&lt;h4 id="export-the-app"&gt;Export the app&lt;/h4&gt;
&lt;p&gt;The testing system needs to be able to control the app a little bit - start it, stop it, and hook into it. To do that, we&amp;rsquo;ll complicate our &lt;code&gt;app.listen&lt;/code&gt; code a bit so that we&amp;rsquo;ve also got a server variable, then we&amp;rsquo;ll export the app and server so out test files can import them. It will end up looking something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;let server&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;process&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;env&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;NODE_ENV &lt;span style="color:#81a1c1"&gt;!==&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;test&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; server &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; app&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;listen&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; console&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;Maths server is running at http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;localhost&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;&lt;span style="color:#eceff4"&gt;{&lt;/span&gt;port&lt;span style="color:#eceff4"&gt;}&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#eceff4"&gt;));}&lt;/span&gt;module&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;exports &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; app&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; server &lt;span style="color:#eceff4"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This stops the server being started here if we&amp;rsquo;re in test mode, but exports the bits the test framework needs to manage things.&lt;/p&gt;
&lt;h4 id="create-the-test-files"&gt;Create the test files&lt;/h4&gt;
&lt;p&gt;Our JS test code is all going in a &lt;code&gt;test/&lt;/code&gt; directory in our project, and they will all be named &lt;code&gt;&amp;lt;something&amp;gt;.test.js&lt;/code&gt; I usually use the file name of the file I&amp;rsquo;m testing. So today I&amp;rsquo;m writing tests for &lt;code&gt;app.js&lt;/code&gt; my tests will be in &lt;code&gt;apps.test.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Each test file will need to pull in our tools (supertest and chai) and the server and app variables.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s followed by one or more &lt;em&gt;test suites&lt;/em&gt;; each test suite contains one or more &lt;em&gt;test cases&lt;/em&gt;. This might be easier to explain if we look at a real file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; supertest &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;supertest&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; chai &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;chai&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; app&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; server &lt;span style="color:#eceff4"&gt;}&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; require&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;../app&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;const&lt;/span&gt; expect &lt;span style="color:#81a1c1"&gt;=&lt;/span&gt; chai&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;expect&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;describe&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;POST / add&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; it&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;should return the correct sum&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; supertest&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;app&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/sum&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; a&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#b48ead"&gt;5&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; b&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#b48ead"&gt;5&lt;/span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;expect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;res &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; expect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sum&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;to&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;equal&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;10&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; it&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;should return the correct sum with negative numbers&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; supertest&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;app&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/sum&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; a&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;&lt;span style="color:#b48ead"&gt;5&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; b&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;&lt;span style="color:#b48ead"&gt;5&lt;/span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;expect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;res &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; expect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sum&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;to&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;equal&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;&lt;span style="color:#b48ead"&gt;10&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;describe&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;POST / multiply&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; it&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;should return the correct product&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;return&lt;/span&gt; supertest&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;app&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;post&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;/multiply&amp;#39;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;send&lt;span style="color:#eceff4"&gt;({&lt;/span&gt; a&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#b48ead"&gt;5&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; b&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#b48ead"&gt;5&lt;/span&gt; &lt;span style="color:#eceff4"&gt;})&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;expect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;200&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;then&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;res &lt;span style="color:#81a1c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt; expect&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;res&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;body&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;product&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;to&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;equal&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;25&lt;/span&gt;&lt;span style="color:#eceff4"&gt;);&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});&lt;/span&gt; &lt;span style="color:#eceff4"&gt;});});&lt;/span&gt;server&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;close&lt;span style="color:#eceff4"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This file contains two test suites - &amp;lsquo;POST/add&amp;rsquo; and &amp;lsquo;POST/multiply&amp;rsquo;. POST/add contains two test cases (each begins with &lt;code&gt;it&amp;lt;statement of what the test subject should do&amp;gt;&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s no end to the tests you can write. I normally do basic functionality as I&amp;rsquo;m writing code, and I also add in tests for anything that emerges as a bug. If you get into the rhythm of bug -&amp;gt; write failing test -&amp;gt; fix bug -&amp;gt; test passes you can have your day punctuated by little doses of dopamine. I often write a timed test - that an endpoint should respond in 10ms. These don&amp;rsquo;t help you when you are developing, but sure will later. You should also check that all of the wrong inputs users will eventually try have been handled. If an API expects a number, check for errors being thrown for strings, for negative numbers, for huge numbers, for decimals, for booleans, for objects etc etc.&lt;/p&gt;
&lt;p&gt;Another thing I will do is use a code coverage tool to check my test covers all the branches and error conditions. I plan to talk about that another day. First I need to show you how to run the tests.&lt;/p&gt;
&lt;h4 id="add-test-script"&gt;Add test script&lt;/h4&gt;
&lt;p&gt;If we had installed mocha globally, we could just call it from the command line with something like:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mocha ./test/app.test.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;But we didn&amp;rsquo;t do that, so we need npm to start it up for us. I know this seems like another time wasting step, but it&amp;rsquo;s one of those do it once, benefit from it thousands of times things.&lt;/p&gt;
&lt;p&gt;In the &lt;code&gt;package.json&lt;/code&gt; file, we can add a section called scripts. If you started you project with &lt;code&gt;npm init&lt;/code&gt; you may already have this section, if not, just add it in. It&amp;rsquo;s common to have a &lt;code&gt;run&lt;/code&gt; and a &lt;code&gt;test&lt;/code&gt; script, and I often have one or two others. Here&amp;rsquo;s the sort of thing you want.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{ &amp;#34;name&amp;#34;: &amp;#34;test-demo&amp;#34;, &amp;#34;version&amp;#34;: &amp;#34;0.1.0&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;Simple Maths API&amp;#34;, &amp;#34;main&amp;#34;: &amp;#34;app.js&amp;#34;, &amp;#34;scripts&amp;#34;: { &amp;#34;start&amp;#34;: &amp;#34;node app.js&amp;#34;, &amp;#34;test&amp;#34;: &amp;#34;mocha &amp;#39;./test/*.test.js&amp;#39;&amp;#34; }, &amp;#34;dependencies&amp;#34;: { &amp;#34;express&amp;#34;: &amp;#34;^4.18.2&amp;#34; }, &amp;#34;devDependencies&amp;#34;: { &amp;#34;chai&amp;#34;: &amp;#34;^4.3.10&amp;#34;, &amp;#34;mocha&amp;#34;: &amp;#34;^10.2.0&amp;#34;, &amp;#34;nyc&amp;#34;: &amp;#34;^15.1.0&amp;#34;, &amp;#34;supertest&amp;#34;: &amp;#34;^6.3.3&amp;#34; }}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;npm&lt;/code&gt; does the magic to make the correct version of the library available when this script is run. The end effect of these is that you can type &lt;code&gt;npm test&lt;/code&gt; at the command line, and mocha will run your tests. Let&amp;rsquo;s try it would our tests.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-8.18.28-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-8.18.28-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what we like to see, passing tests. I&amp;rsquo;ll make one fail by telling it to expect 5x5=26.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-8.22.53-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-12-16-at-8.22.53-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it, you&amp;rsquo;re all set up to write tests against your node apps.&lt;/p&gt;
&lt;h3 id="what-do-the-different-bits-do"&gt;What do the different bits do?&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s a lot of moving parts here, lets tease those out a little.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mochajs.org/#getting-started"&gt;mocha&lt;/a&gt; - this is the test framework. As we&amp;rsquo;ve discussed, it&amp;rsquo;s the command line tool that runs the tests and produces the output.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.npmjs.com/package/supertest"&gt;supertest&lt;/a&gt; - manages the connections between the test runner/framework and the code being tested. When I&amp;rsquo;m pressing a button in Bruno, it&amp;rsquo;s actually hitting localhost:3000 to exercise the server which I&amp;rsquo;ve previously started. supertest is doing magic to make that connection without going through the network layers.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.chaijs.com"&gt;chai&lt;/a&gt; - it provides the assert()s, expect()s and should()s that we use in the test cases. You could, in theory make do with the assert() library built into node - especially for our toy demo app - but it&amp;rsquo;s no where near as nice, and in particular chai has a massive set of plugins that both extend it&amp;rsquo;s use generally, but also into working at a detailed level with other vendor packages.&lt;/p&gt;</description></item><item><title>Bruno asserts</title><link>https://blog.iankulin.com/bruno-asserts/</link><pubDate>Sat, 11 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/bruno-asserts/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-12.11.09-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-12.11.09-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I mentioned &lt;a href="https://www.usebruno.com/"&gt;Bruno&lt;/a&gt; the other day. Although it&amp;rsquo;s still very much under development, it is shaping up as a great Postman/Insomnia replacement.&lt;/p&gt;
&lt;p&gt;One of the aspects I&amp;rsquo;ve been using today is asserts. As part of a request, you can add some asserts - so when you&amp;rsquo;re hitting an endpoint it will check what status should it be returning, or given the data you&amp;rsquo;re passing in, what should be in the response body.&lt;/p&gt;
&lt;p&gt;When I&amp;rsquo;d asked ChatGPT to to review the mdserver code, it had suggested that I should be sanitising URL inputs better to prevent users transversing out of the &amp;lsquo;public&amp;rsquo; file directory to other places in the file system. I thought Express had already taken care of this for me, but wanted to check. I had ChatGPT generate a bunch of pass and fail URL examples, then just created asserts for each one in Bruno.&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s done, you can just right click on the collection and have it run all of those.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-12.19.59-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;An extra benefit of Bruno is that all these requests are stored as JSON-like, version-controllable text. I store them in my project and commit them along with the rest of my code.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-12.25.43-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-22-at-12.25.43-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Testing, testing</title><link>https://blog.iankulin.com/testing-testing/</link><pubDate>Sun, 09 Oct 2022 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/testing-testing/</guid><description>&lt;p&gt;I have unit testing in my goals, and if I&amp;rsquo;m going to throw this &lt;a href="https://blog.iankulin.com/codetrimmer-first-macos-app/"&gt;space trimming&lt;/a&gt; macOS utility up on the web, now might be a good time to figure out how to add unit tests to a project, how to write them, and how to run them. XCode is well set up for this, so it&amp;rsquo;s really no drama to do.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-9.09.32-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Although I haven&amp;rsquo;t worried with any unit testing up to this point in my iOS/Swift learning, in my previous programming work I did a lot of work with the large calculations involved in translating GPS coordinates and robotic positioning models where errors would be bad - so I&amp;rsquo;ve written a lot of tests over the years. I&amp;rsquo;ve also definitely felt the confidence you can dramatically refactor code with when you know the code has a robust test suite. I&amp;rsquo;m a big fan.&lt;/p&gt;
&lt;h4 id="adding-tests-to-existing-project"&gt;Adding Tests to Existing Project&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-9.28.39-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2022-10-03-at-9.28.39-pm.png" width="239" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In Xcode, with your project open, you need to add the &lt;em&gt;testing target&lt;/em&gt;. &lt;code&gt;File | New | Target...&lt;/code&gt; then scroll down to find the &lt;em&gt;Unit Testing Bundle&lt;/em&gt;. When you add that, you&amp;rsquo;ll see a new folder in the Project Navigator, and a new source file - both with the name &lt;code&gt;&amp;lt;app name&amp;gt;Tests&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you click into the &lt;code&gt;&amp;lt;app name&amp;gt;Test.swift&lt;/code&gt; file, you&amp;rsquo;ll see a couple of methods for setting up the testing environment and cleaning up after it. They are called before and after each test - you&amp;rsquo;d use them if you needed to set something up like some files in a directory or similar and wanted to ensure it was not affected by the tests run before the current test. Don&amp;rsquo;t worry about them for the time being.&lt;/p&gt;
&lt;h4 id="writing-tests"&gt;Writing Tests&lt;/h4&gt;
&lt;p&gt;Before we can write any tests, we need to import any modules with the code we want to test. In my case, I want to test the function stripSpaces() which is in the ContentView of my app called CodeTrimmer. So in the top of the CodeTrimmerTests.swift file, under the other import, I add the import:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;import XCTest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@testable import CodeTrimmer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then we can write our tests. The test functions must start with the magic prefix &lt;em&gt;test&lt;/em&gt; so that Xcode treats them as such. The usual approach in the test is to call a function with some specific input, then test the output is what is expected with a special test version of assert():&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a couple. My function stripSpaces() deletes a number of spaces from each line of a block of code such that the block becomes left aligned. if the XCTAsserts() pass, the test passes and I get a green tick. To run the tests I just click in the breakpoint gutter next to the function declaration.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; &lt;span style="color:#88c0d0"&gt;testStripSpaces01&lt;/span&gt;&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;throws&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; testString &lt;span style="color:#eceff4"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a3be8c"&gt; single line string no spaces
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a3be8c"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; resultString &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; stripSpaces&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;testString&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; XCTAssertTrue&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;resultString &lt;span style="color:#eceff4"&gt;==&lt;/span&gt; testString&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; testString&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;func&lt;/span&gt; &lt;span style="color:#88c0d0"&gt;testStripSpaces02&lt;/span&gt;&lt;span style="color:#eceff4"&gt;()&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;throws&lt;/span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; testString &lt;span style="color:#eceff4"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a3be8c"&gt; single line 4 spaces
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a3be8c"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;let&lt;/span&gt; resultString &lt;span style="color:#eceff4"&gt;=&lt;/span&gt; stripSpaces&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;testString&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; XCTAssertFalse&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;resultString &lt;span style="color:#eceff4"&gt;==&lt;/span&gt; testString&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; testString&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; XCTAssertTrue&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;testString&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;dropFirst&lt;/span&gt;&lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;4&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#eceff4"&gt;==&lt;/span&gt; resultString&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; testString&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item></channel></rss>