<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Tyler Hawkins</title>
    <description>The latest articles on Forem by Tyler Hawkins (@thawkin3).</description>
    <link>https://forem.com/thawkin3</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F344735%2F70749f3f-334b-4414-a6e2-ca8dd8a89f3a.jpeg</url>
      <title>Forem: Tyler Hawkins</title>
      <link>https://forem.com/thawkin3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/thawkin3"/>
    <language>en</language>
    <item>
      <title>AI Build Traps: Usage, Output, and Outcomes (How perverse incentives around token usage create cobra farms)</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Thu, 26 Mar 2026 04:27:04 +0000</pubDate>
      <link>https://forem.com/thawkin3/ai-build-traps-usage-output-and-outcomes-how-perverse-incentives-around-token-usage-create-4cii</link>
      <guid>https://forem.com/thawkin3/ai-build-traps-usage-output-and-outcomes-how-perverse-incentives-around-token-usage-create-4cii</guid>
      <description>&lt;p&gt;Organizations have long struggled with answering the question: "How do we measure developer productivity?"&lt;/p&gt;

&lt;p&gt;In the past, some organizations have taken to measuring lines of code produced per engineer. Others have measured the number of tickets closed. Others measure the number of pull requests (PRs) merged.&lt;/p&gt;

&lt;p&gt;And now in the age of AI, we've started measuring token usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proxy metrics
&lt;/h2&gt;

&lt;p&gt;The problem with all of these metrics is that they're not actually measurements of developer productivity — they're proxy metrics. And with any proxy metric, once you start measuring something, that metric becomes the goal. This is especially true if that metric has financial incentives or performance ratings tied to it.&lt;/p&gt;

&lt;p&gt;Measuring lines of code? Great, engineers will start writing overly verbose PRs in order to write more lines. Goodbye concise functions and reusable code.&lt;/p&gt;

&lt;p&gt;Measuring number of tickets closed? Great, engineers will start creating tickets for &lt;em&gt;everything&lt;/em&gt;, and they'll break tasks down into the smallest tickets possible. Now we're more focused on Jira and paperwork than actual work and value provided.&lt;/p&gt;

&lt;p&gt;Measuring number of pull requests merged? Great, engineers will focus on creating as many PRs as possible, with the smallest changes they can think of, or with the most trivial updates.&lt;/p&gt;

&lt;p&gt;Measuring token usage? Great, engineers will use their AI assistants for everything to use more tokens, regardless of whether or not the output is useful or makes the engineer more efficient.&lt;/p&gt;

&lt;p&gt;All of these examples are what are called &lt;strong&gt;perverse incentives&lt;/strong&gt; — incentives that create unintended and oftentimes counterproductive behaviors that work contrary to the desired goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cobra farms
&lt;/h2&gt;

&lt;p&gt;One of the most famous examples of perverse incentives is the story of the cobra problem in India. According to the story, Delhi was overrun with cobras, venomous snakes. The government wanted to get rid of the cobras, so they offered a reward for every dead cobra that someone brought in. Seems reasonable, right? The more dead cobras there are, the less live ones there are.&lt;/p&gt;

&lt;p&gt;But can you see the perverse incentive? People were rewarded for dead cobras. How do you get more dead cobras, besides finding more? You raise more cobras yourself and then kill them. So people began breeding cobras themselves so that they could turn them in for the reward.&lt;/p&gt;

&lt;p&gt;And then, once the government realized what was happening, the government ended the incentive program. Cobra breeders no longer had any reason to breed more cobras, so they released the rest of what they had into the wild, resulting in even more cobras in the city.&lt;/p&gt;

&lt;p&gt;The well-intentioned goal of rewarding people for dead cobras actually resulted in more cobras being present, not fewer.&lt;/p&gt;

&lt;p&gt;(For the record, whether or not this story is true or if parts are exaggerated is not entirely certain. But it's a very fun example for a teaching moment!)&lt;/p&gt;

&lt;h2&gt;
  
  
  AI usage
&lt;/h2&gt;

&lt;p&gt;So now let's get back to AI usage and tokens. Organizations are under more pressure than ever to prove that they're thriving in this brave new world of AI. Engineers are under that same pressure, expected to do more and accomplish more work. That's the promise we've all been sold, that AI will make us more productive.&lt;/p&gt;

&lt;p&gt;But how do we measure this increased productivity? Well, we're back to the same problem organizations have always had, even before AI. It's hard to measure productivity, so we come up with a proxy metric, and that proxy metric is now token usage.&lt;/p&gt;

&lt;p&gt;Surely if an engineer is using more tokens, that means they're using AI more, so they're more productive in their job and helping the business even more.… Right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage, output, and outcomes
&lt;/h2&gt;

&lt;p&gt;That's where the build trap happens. Organizations go astray when they focus more on &lt;strong&gt;output&lt;/strong&gt; (e.g. pull requests merged, features shipped, projects completed) than on &lt;strong&gt;outcomes&lt;/strong&gt; (e.g. value delivered to customers, user behavior change, pain points resolved).&lt;/p&gt;

&lt;p&gt;Maybe token usage is increasing, but does that correspond to an increase in pull requests and actual work done? If token usage increases but the output of the individual stays the same, then we haven't actually accomplished anything. We're just celebrating using more tokens (spending more money) but with no actual increased productivity.&lt;/p&gt;

&lt;p&gt;And taking that a step further, let's say that we do see an increase in pull requests merged, which presumably is a good thing. But does that increase in output lead to a corresponding increased benefit in outcomes? Is the product better because of the increased output?&lt;/p&gt;

&lt;p&gt;In other words, does the increased code result in a tangible increase in the value that the company provides to its users?&lt;/p&gt;

&lt;p&gt;That question is far more difficult to answer, but far more important.&lt;/p&gt;

&lt;p&gt;Mediocre organizations measure usage and output and then stop there. Great organizations measure outcomes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Test-Driven Development for Building User Interfaces</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Mon, 09 Feb 2026 16:00:00 +0000</pubDate>
      <link>https://forem.com/thawkin3/test-driven-development-for-building-user-interfaces-4k6d</link>
      <guid>https://forem.com/thawkin3/test-driven-development-for-building-user-interfaces-4k6d</guid>
      <description>&lt;p&gt;Test-driven development, or TDD, is a programming paradigm in which you write your tests first and your source code second. TDD is perfect when you’re writing code that has clear inputs and outputs, like pure functions or API endpoints.&lt;/p&gt;

&lt;p&gt;But what about when building user interfaces? Can TDD be done for UI development?&lt;/p&gt;

&lt;p&gt;You’re about to find out!&lt;/p&gt;

&lt;p&gt;In this article we’ll explore a few questions while building a demo UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Can&lt;/em&gt;&lt;/strong&gt; we use TDD to build UIs?&lt;/li&gt;
&lt;li&gt;If so, &lt;strong&gt;&lt;em&gt;how&lt;/em&gt;&lt;/strong&gt; do we do it?&lt;/li&gt;
&lt;li&gt;And finally, &lt;strong&gt;&lt;em&gt;should&lt;/em&gt;&lt;/strong&gt; we use TDD to build UIs?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Note: This article was inspired by a conference talk I gave in January 2021 at THAT Conference Texas. This article was also written at that time. It’s been five years now, so some practices have changed. (Do most frontend engineers even know what Enzyme is anymore, or did they start out with React Testing Library?) However, I still think this is an interesting thought experiment and worth discussing, hence why I’m finally publishing this in its original form.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Background Motivation
&lt;/h2&gt;

&lt;p&gt;When discussing test-driven development with frontend developers, the conversation usually goes something like this:&lt;/p&gt;

&lt;p&gt;“Yeah TDD is great for simple functions or backend work, but it just doesn’t make sense for frontend work. When I’m building my UI, I don’t know what code I’ll end up writing. I have no idea if I’ll end up using a &lt;code&gt;div&lt;/code&gt; or a &lt;code&gt;span&lt;/code&gt; or a &lt;code&gt;p&lt;/code&gt; element here. TDD for UIs just isn’t feasible.”&lt;/p&gt;

&lt;p&gt;However, I’d like to argue that using TDD to build UIs isn’t as hard as we may think.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ideal Conditions for TDD
&lt;/h2&gt;

&lt;p&gt;Ideally, we’d use TDD to write our code when the following two conditions are true:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We have clear project requirements&lt;/li&gt;
&lt;li&gt;We have clear inputs and outputs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If those two requirements are not met, it’s difficult or nearly impossible to use TDD. So let’s examine those two requirements in the context of frontend development.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clear Project Requirements
&lt;/h3&gt;

&lt;p&gt;When you’re developing a new feature, you’re typically given mockups by a UX designer. These mockups show you how the feature should look and how the feature should behave. For example, “when the user clicks this button, a dialog modal appears on the screen.”&lt;/p&gt;

&lt;p&gt;Good mockups will clarify various details such as how inputs will look when in a hover or focus state, how empty states will look when content is missing, and how the page layout will change for desktop, laptop, and mobile screen sizes.&lt;/p&gt;

&lt;p&gt;As you may have already guessed, the mockups provide the project requirements! We know exactly how our UI should look and behave. If there’s anything unclear in the mockups, engineers should ask clarifying questions with their UX designer or product manager so that the requirements are absolutely clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clear Inputs and Outputs
&lt;/h3&gt;

&lt;p&gt;Now, what about clear inputs and outputs?&lt;/p&gt;

&lt;p&gt;Most frontend engineers these days use a UI library or framework like React or Angular. A UI library like React allows you to build reusable components to create small building blocks of functionality that you can piece together to make an app.&lt;/p&gt;

&lt;p&gt;Now, what is a component? Well, in React, it’s a function! Components are simply functions of props and state that return a piece of UI. So we have clear inputs and outputs!&lt;/p&gt;

&lt;p&gt;Given the same props and state, a component will always render the same thing. Components are deterministic, and as long as they don’t kick off side effects like making an API request, they are pure functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Considerations
&lt;/h2&gt;

&lt;p&gt;So, in theory, using TDD to build UIs &lt;em&gt;should work&lt;/em&gt;. Both of our ideal conditions are met.&lt;/p&gt;

&lt;p&gt;But what about the unknowns? As mentioned above, we still might not know a few things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Component props and state we’ll use&lt;/li&gt;
&lt;li&gt;Names we’ll give our methods and functions&lt;/li&gt;
&lt;li&gt;HTML elements we’ll use&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But we &lt;em&gt;do&lt;/em&gt; know how the UI should look and behave. I’d argue that the unknown implementation details actually don’t matter.&lt;/p&gt;

&lt;p&gt;This outdated way of thinking about testing implementation details largely stems from Airbnb’s testing library &lt;a href="https://enzymejs.github.io/enzyme/" rel="noopener noreferrer"&gt;Enzyme&lt;/a&gt;. Enzyme allowed you to dive into the internals of your React components, trigger class component methods, and manually update a component’s props and state.&lt;/p&gt;

&lt;p&gt;However, none of those are things that a user can do. A user can only interact with your app through the interface that you provide. For example, the user might click on a button or fill out a form field.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://testing-library.com/docs/react-testing-library/intro/" rel="noopener noreferrer"&gt;React Testing Library&lt;/a&gt;’s core philosophy is that we should write our tests in such a way that we simulate user behavior. By testing what the user can actually do, our tests focus less on implementation details and more on the actual user interface, which leads to less brittle tests and a more reliable test suite.&lt;/p&gt;

&lt;p&gt;The key here is that React Testing Library actually facilitates using TDD to build UIs by taking the focus away from the implementation details.&lt;/p&gt;

&lt;p&gt;Remember: the unknown implementation details don’t matter. What matters is how the UI looks and behaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Time: Building a Request Form
&lt;/h2&gt;

&lt;p&gt;Alright, enough theory. I hope by now I’ve presented a clear case that it should be possible to build a user interface using test-driven development.&lt;/p&gt;

&lt;p&gt;But how does this actually look in practice?&lt;/p&gt;

&lt;p&gt;To illustrate these concepts in a concrete way, we’ll build a simple UI in React using TDD. Let’s imagine that we need to create a request form that allows visitors on our site to request a demo of our new product. Here are the mockups that we’ll pretend we’ve been given:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F0%2AsziVE-skkQqA-B_l" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F0%2AsziVE-skkQqA-B_l" alt="Mockup 1: Initial UI" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;Mockup 1: Initial UI&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the form displays three inputs for a user’s first name, last name, and email address. There is a submit button at the bottom of the form.&lt;/p&gt;

&lt;p&gt;If a user tries to submit the form without filling out all three fields, an error message will be displayed next to each invalid input:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F0%2AbJ-4AvPyyuFUCC-S" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F0%2AbJ-4AvPyyuFUCC-S" alt="Mockup 2: Error messages appear on form submission if fields are left blank" width="926" height="686"&gt;&lt;/a&gt;&lt;em&gt;Mockup 2: Error messages appear on form submission if fields are left blank&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Finally, once the user properly fills out and submits the form, a confirmation screen is shown:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F0%2AKxMcxuMB43xAGNcU" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F0%2AKxMcxuMB43xAGNcU" alt="Mockup 3: Confirmation text is shown after the form is successfully submitted" width="956" height="278"&gt;&lt;/a&gt;&lt;em&gt;Mockup 3: Confirmation text is shown after the form is successfully submitted&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;We’ll begin by bootstrapping a brand new app using &lt;code&gt;create-react-app&lt;/code&gt;. Once my app was generated, I deleted and re-arranged some of the initial code to get to a good starting point. As you can see, we’ll start with a bare bones app that only returns a header element.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe7h5y0tdw4p27yddh4uf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe7h5y0tdw4p27yddh4uf.png" alt="App starting point" width="800" height="166"&gt;&lt;/a&gt;&lt;em&gt;App starting point&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can follow along here on the &lt;a href="https://github.com/thawkin3/tdd-for-ui-demo/tree/demo/start" rel="noopener noreferrer"&gt;demo/start&lt;/a&gt; branch of my &lt;a href="https://github.com/thawkin3/tdd-for-ui-demo" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test #1: Sanity Check
&lt;/h2&gt;

&lt;p&gt;Let’s start by creating a &lt;code&gt;RequestForm.test.js&lt;/code&gt; file. We’ll write our first test that asserts that our &lt;code&gt;RequestForm&lt;/code&gt; component renders without crashing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RequestForm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./RequestForm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RequestForm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renders without crashing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RequestForm&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toThrow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you might have guessed, this test fails because we don’t actually have a &lt;code&gt;RequestForm.js&lt;/code&gt; file or a &lt;code&gt;RequestForm&lt;/code&gt; component created yet. And that’s ok, because this is how TDD works — we write our tests first and then our source code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqdes19cgz1a2aj7u7zid.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqdes19cgz1a2aj7u7zid.png" alt="Our first test fails because the RequestForm.js file does not exist" width="800" height="404"&gt;&lt;/a&gt;&lt;em&gt;Our first test fails because the RequestForm.js file does not exist&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s now write some code to get this test to pass. We’ll create a &lt;code&gt;RequestForm.js&lt;/code&gt; file that exports a &lt;code&gt;RequestForm&lt;/code&gt; component. The &lt;code&gt;RequestForm&lt;/code&gt; component will simply render a &lt;code&gt;form&lt;/code&gt; HTML element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RequestForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when we run our tests, they pass!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fih12og7nf6ab8pag35mq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fih12og7nf6ab8pag35mq.png" alt="Our first test passes now" width="572" height="238"&gt;&lt;/a&gt;&lt;em&gt;Our first test passes now&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Test #2: First Name Input
&lt;/h2&gt;

&lt;p&gt;Let’s move on to our second test. Let’s assert that our &lt;code&gt;RequestForm&lt;/code&gt; component renders an input for the user to enter their first name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renders a first name text input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RequestForm&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we’ll run our tests, and… this new test fails, as expected, because our &lt;code&gt;RequestForm&lt;/code&gt; component doesn’t render any inputs yet. It’s still just an empty form.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqke2zj95wh050zeszhx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqke2zj95wh050zeszhx.png" alt="Our second test fails because the RequestForm component does not yet render an input for the first name" width="800" height="517"&gt;&lt;/a&gt;&lt;em&gt;Our second test fails because the RequestForm component does not yet render an input for the first name&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We can update our &lt;code&gt;RequestForm&lt;/code&gt; component to render an &lt;code&gt;input&lt;/code&gt; element and an accompanying &lt;code&gt;label&lt;/code&gt; element that allows the user to enter their first name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RequestForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;First Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when we run our tests, they all pass!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ndq5zr3q8ruylmq9myh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ndq5zr3q8ruylmq9myh.png" alt="Our second test passes now that the form renders an input for the first name" width="678" height="266"&gt;&lt;/a&gt;&lt;em&gt;Our second test passes now that the form renders an input for the first name&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here’s what our current app’s UI looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flwlv5lnm7qkkg79o0bp8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flwlv5lnm7qkkg79o0bp8.png" alt="App renders a First Name input" width="800" height="165"&gt;&lt;/a&gt;&lt;em&gt;App renders a First Name input&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It’s not a very pretty app, but we’ll come back to style it up in a few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test #3: Last Name Input
&lt;/h2&gt;

&lt;p&gt;Now let’s write another test that looks just like our test for the “first name” input, but this time let’s assert that there is a “last name” input on the page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renders a last name text input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RequestForm&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Last Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll run our tests, and, you guessed it, this new test fails. No surprises there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxj93wuof2vy3huwhaew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxj93wuof2vy3huwhaew.png" alt="Our third test fails because the RequestForm component does not yet render an input for the last name" width="800" height="687"&gt;&lt;/a&gt;&lt;em&gt;Our third test fails because the RequestForm component does not yet render an input for the last name&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So, just like before, let’s update our &lt;code&gt;RequestForm&lt;/code&gt; component to render another &lt;code&gt;input&lt;/code&gt; element and another &lt;code&gt;label&lt;/code&gt; element, but this time for the user to enter their last name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RequestForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;First Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Last Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll run our tests again, and now they all pass. Nice!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftuc4t5y0xjns3oujm9h9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftuc4t5y0xjns3oujm9h9.png" alt="Our third test passes now that the form renders an input for the last name" width="706" height="292"&gt;&lt;/a&gt;&lt;em&gt;Our third test passes now that the form renders an input for the last name&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here’s what our app’s UI looks like now:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2wyykgh2nc1xmzlytqs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2wyykgh2nc1xmzlytqs.png" alt="App renders a First Name input and a Last Name input" width="800" height="187"&gt;&lt;/a&gt;&lt;em&gt;App renders a First Name input and a Last Name input&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not very pretty, is it? With all of our tests currently passing, this seems like a great time for a small refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactor #1: Rendering Labels and Inputs on Individual Rows
&lt;/h2&gt;

&lt;p&gt;Before we jump into our refactor, I’d like to call out a key principle here. When doing test-driven development, you follow what’s called a “red, green, refactor” cycle.&lt;/p&gt;

&lt;p&gt;You start by writing a failing test, so you’re in the red. You then write the source code to make the test pass, which puts you in the green. Once you’re in the green, you can refactor your code all you want. At the end of your refactor, your existing tests must still pass, keeping you in the green.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F0%2Aqa1bGIbpAL2P23qs" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F0%2Aqa1bGIbpAL2P23qs" alt="Red, green, refactor cycle by Nat Pryce" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;Red, green, refactor cycle by Nat Pryce&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When it comes to UI development, we can consider style changes a refactor.&lt;/p&gt;

&lt;p&gt;With that context provided, let’s now style up our app a little. It’d be nice to have our label-input pairs on separate rows. We could also add some breathing room between the label and the input text box.&lt;/p&gt;

&lt;p&gt;To do this, we’ll add a &lt;code&gt;div&lt;/code&gt; element with a &lt;code&gt;formGroup&lt;/code&gt; class wrapped around our label-input pairs. We’ll also create a &lt;code&gt;RequestForm.css&lt;/code&gt; file to include some CSS.&lt;/p&gt;

&lt;p&gt;Here’s the &lt;code&gt;RequestForm.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./RequestForm.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RequestForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"formGroup"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;First Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"formGroup"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Last Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here’s the accompanying &lt;code&gt;RequestForm.css&lt;/code&gt; file that we import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.formGroup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.formGroup&lt;/span&gt; &lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With those changes in place, our UI now looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3316%2F1%2ATCNJNXSa7gselJXwGQZ-0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3316%2F1%2ATCNJNXSa7gselJXwGQZ-0w.png" alt="App with the label-input pairs on their own rows" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;App with the label-input pairs on their own rows&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Much better! There is still more we can do style our form to make it match our mockups, but we’ll call it good for now.&lt;/p&gt;

&lt;p&gt;Now, remember what we had learned about the red, green, refactor cycle? Refactors should happen while we’re in the green, and after we’re done with our refactor we should still be in the green. Let’s run our tests to see how things are looking:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AHfiUXJnF7yPr82IVwE08mg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AHfiUXJnF7yPr82IVwE08mg.png" alt="Our tests are still passing after our style refactor" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;Our tests are still passing after our style refactor&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sweet! Our tests are still passing after our style refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test #4: Email Address Input
&lt;/h2&gt;

&lt;p&gt;Let’s move on to our next test. We have tests for the “first name” input and the “last name” input, so let’s now write one more test for the “email address” input.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renders an email address text input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RequestForm&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fails, as expected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2784%2F1%2A1x7OSDqT5DXbRUoB1VRISA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2784%2F1%2A1x7OSDqT5DXbRUoB1VRISA.png" alt="Our fourth test fails because the RequestForm component does not yet render an input for the email address" width="800" height="1004"&gt;&lt;/a&gt;&lt;em&gt;Our fourth test fails because the RequestForm component does not yet render an input for the email address&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Just like we did with the “first name” and “last name” inputs, we can add another &lt;code&gt;input&lt;/code&gt; element and another &lt;code&gt;label&lt;/code&gt; element so that the user can enter their email address.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./RequestForm.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RequestForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"formGroup"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;First Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"formGroup"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Last Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"formGroup"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, our tests will all pass again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2ADrVCY15ECsNtKrDSuLO39A.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2ADrVCY15ECsNtKrDSuLO39A.png" alt="Our fourth test passes now that the form renders an input for the email address" width="708" height="330"&gt;&lt;/a&gt;&lt;em&gt;Our fourth test passes now that the form renders an input for the email address&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s check out the UI as it stands now:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3324%2F1%2AK-7i1bsnRcyRVq_C-my_0Q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3324%2F1%2AK-7i1bsnRcyRVq_C-my_0Q.png" alt="App renders inputs for first name, last name, and email address" width="800" height="206"&gt;&lt;/a&gt;&lt;em&gt;App renders inputs for first name, last name, and email address&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Our inputs are still in rows, but the alignment on the “email” field could look better. It’d appear more uniform if all of the input boxes lined up nicely as if they were in a column. Let’s address this with a second refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactor #2: Lining Up the Columns
&lt;/h2&gt;

&lt;p&gt;To better align our inputs, we can add a couple simple CSS rules to our existing styles. We’ll add a &lt;code&gt;width&lt;/code&gt; and a &lt;code&gt;display&lt;/code&gt; property to our &lt;code&gt;label&lt;/code&gt; elements so that our full CSS file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.formGroup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.formGroup&lt;/span&gt; &lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now our UI looks just a little nicer:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3292%2F1%2ATAqxkX-w8NzvF0mukZygBw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3292%2F1%2ATAqxkX-w8NzvF0mukZygBw.png" alt="App with all form inputs aligned" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;App with all form inputs aligned&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And, the important part during any refactor: the tests all still pass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AmBNnjc4Ozf62djYlwrDVfg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AmBNnjc4Ozf62djYlwrDVfg.png" alt="Our tests are still passing after our second style refactor" width="710" height="322"&gt;&lt;/a&gt;&lt;em&gt;Our tests are still passing after our second style refactor&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Test #5: Submit Button
&lt;/h2&gt;

&lt;p&gt;Let’s move on to our next test. This test will assert that a submit button with the text “Request Demo” appears in the form.&lt;/p&gt;

&lt;p&gt;As I’m sure you can figure out by now, the test fails of course because we haven’t implemented the submit button yet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3180%2F1%2ARoigHKxDg8EIHytBV2r9-A.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3180%2F1%2ARoigHKxDg8EIHytBV2r9-A.png" alt="Our fifth test fails because the RequestForm component does not yet render a submit button" width="800" height="1123"&gt;&lt;/a&gt;&lt;em&gt;Our fifth test fails because the RequestForm component does not yet render a submit button&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We can easily add the submit button to our &lt;code&gt;RequestForm&lt;/code&gt; component like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./RequestForm.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RequestForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"formGroup"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;First Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"formGroup"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Last Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"formGroup"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Request Demo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the tests pass again, putting us back in the green.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AwrZmKtpTRWdO1UlW1yoUFA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AwrZmKtpTRWdO1UlW1yoUFA.png" alt="Our fifth test passes now that the form renders a submit button" width="710" height="346"&gt;&lt;/a&gt;&lt;em&gt;Our fifth test passes now that the form renders a submit button&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And the app’s UI now shows the “Request Demo” button too:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3280%2F1%2AMpo_Fs6OGz2weZ1qCY3fwA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3280%2F1%2AMpo_Fs6OGz2weZ1qCY3fwA.png" alt="App renders the three inputs and the submit button" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;App renders the three inputs and the submit button&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactor #3: Centering and Styling the Form
&lt;/h2&gt;

&lt;p&gt;If we look back at our design mockups, we’ll see that our form should be centered on the page and should have a thin black border around it. Since all our tests are passing and the form contents are complete, now seems like a good time to do a third refactor with some more style updates.&lt;/p&gt;

&lt;p&gt;In our &lt;code&gt;RequestForm.js&lt;/code&gt; file, we’ll add a &lt;code&gt;requestForm&lt;/code&gt; class to our &lt;code&gt;form&lt;/code&gt; element like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"requestForm"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* the rest of the existing code here */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we’ll add some styles to that class in our &lt;code&gt;RequestForm.css&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.requestForm&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;0.0125rem&lt;/span&gt; &lt;span class="m"&gt;#000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, in our main &lt;code&gt;index.css&lt;/code&gt; file that we haven’t touched at all during this exercise, we’ll add &lt;code&gt;text-align: center&lt;/code&gt; on the &lt;code&gt;body&lt;/code&gt; element. The resulting &lt;code&gt;index.css&lt;/code&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;-apple-system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BlinkMacSystemFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"Segoe UI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"Roboto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;"Oxygen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"Ubuntu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"Cantarell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"Fira Sans"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"Droid Sans"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;"Helvetica Neue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-font-smoothing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;antialiased&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;-moz-osx-font-smoothing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grayscale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, our form is nice and centered on the page with a simple border:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AQwBcVJpWuOi9LlqM7SLyTA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AQwBcVJpWuOi9LlqM7SLyTA.png" alt="App renders a nice form centered on the page, matching the design mockups" width="800" height="488"&gt;&lt;/a&gt;&lt;em&gt;App renders a nice form centered on the page, matching the design mockups&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And, as always, we made sure that our refactors did not break any of our existing tests:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AnasNpyapeLxkOs4vBuAHHw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AnasNpyapeLxkOs4vBuAHHw.png" alt="Our tests are still passing after our third style refactor" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;Our tests are still passing after our third style refactor&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Test #6: Error Message for Invalid Inputs
&lt;/h2&gt;

&lt;p&gt;Now that we have a nicely styled form on our page, it’s time to start focusing on the behavior of the form. As we can see in our second design mockup, we need error messages to display when the form is submitted if any of the input fields are not filled out.&lt;/p&gt;

&lt;p&gt;Let’s first write a test to assert that error messages are shown when the form is submitted with invalid inputs.&lt;/p&gt;

&lt;p&gt;At the top of our &lt;code&gt;RequestForm.test.js&lt;/code&gt; file we’ll need to import a new method from React Testing Library called &lt;code&gt;fireEvent&lt;/code&gt; so that we can click on the submit button as part of our test.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fireEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can write our new test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renders an error message for each of the inputs if none of them are filled out and the user submits the form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RequestForm&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fireEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Request Demo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First Name field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Last Name field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we render the form, click the submit button, and then assert that all three input fields have an accompanying error message shown.&lt;/p&gt;

&lt;p&gt;When we run our tests, this new test of course fails.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3560%2F1%2ATub_v4t8UegTJ2INgg4Pww.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3560%2F1%2ATub_v4t8UegTJ2INgg4Pww.png" alt="Our sixth test fails because the RequestForm component does not yet handle the form submission or show error messages" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;Our sixth test fails because the RequestForm component does not yet handle the form submission or show error messages&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To implement this functionality, we’ll make some fairly significant changes to our &lt;code&gt;RequestForm&lt;/code&gt; component. First, we’ll change our uncontrolled inputs to be controlled inputs. In other words, we’ll let React manage each input’s &lt;code&gt;value&lt;/code&gt; attribute and &lt;code&gt;onChange&lt;/code&gt; handler. Second, we’ll add some error messages that are conditionally rendered when the user submits the form.&lt;/p&gt;

&lt;p&gt;These changes result in the &lt;code&gt;RequestForm.js&lt;/code&gt; file now looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./RequestForm.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RequestForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFirstName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLastName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firstName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;setFirstName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lastName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;setLastName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;firstNameError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFirstNameError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lastNameError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLastNameError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmailError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;setFirstNameError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First Name field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setLastNameError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Last Name field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setEmailError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"requestForm"&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"formGroup"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;First Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;
          &lt;span class="na"&gt;data-testid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;firstNameError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;firstNameError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"formGroup"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Last Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;
          &lt;span class="na"&gt;data-testid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastNameError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastNameError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"formGroup"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;data-testid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Request Demo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was a lot to take in! Feel free to pause and take a minute to look through the code above if you need to.&lt;/p&gt;

&lt;p&gt;Let’s look at our tests now that we’ve implemented the error message functionality:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3164%2F1%2AJWu9jleFOBR7bFfCsW44xA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3164%2F1%2AJWu9jleFOBR7bFfCsW44xA.png" alt="Our sixth test passes now that the form handles submission and validation" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;Our sixth test passes now that the form handles submission and validation&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;They all pass! Beautiful! And how about the app’s UI? Here’s what the app looks like now after the user clicks the submit button without filling out any of the form fields:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AbpT1Jtf5YwFI74nCS9S8bA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AbpT1Jtf5YwFI74nCS9S8bA.png" alt="App displays unformatted error messages if the form is submitted with all empty fields" width="800" height="665"&gt;&lt;/a&gt;&lt;em&gt;App displays unformatted error messages if the form is submitted with all empty fields&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As you can see, while the form is functional, it could use some style updates. That means it’s time for another refactor!&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactor #4: Styling the Error Messages
&lt;/h2&gt;

&lt;p&gt;Looking back at our design mockups, we see that the error messages should be in red. The height of the form shouldn’t change when an error message is displayed either.&lt;/p&gt;

&lt;p&gt;We can achieve this by adding a new class called &lt;code&gt;errorMessage&lt;/code&gt; to our error message content. We will also conditionally add an &lt;code&gt;error&lt;/code&gt; class to our &lt;code&gt;formGroup&lt;/code&gt; element when the input is in an invalid state.&lt;/p&gt;

&lt;p&gt;The resulting &lt;code&gt;RequestForm.js&lt;/code&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./RequestForm.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RequestForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFirstName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLastName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firstName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;setFirstName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lastName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;setLastName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;firstNameError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFirstNameError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lastNameError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLastNameError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmailError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setFirstNameError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First Name field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setLastNameError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Last Name field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setEmailError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"requestForm"&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`formGroup&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;firstNameError&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;First Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;
          &lt;span class="na"&gt;data-testid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;firstNameError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"errorMessage"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;firstNameError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`formGroup&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastNameError&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Last Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;
          &lt;span class="na"&gt;data-testid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastNameError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"errorMessage"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastNameError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`formGroup&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;data-testid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"errorMessage"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Request Demo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the resulting &lt;code&gt;RequestForm.css&lt;/code&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.formGroup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.formGroup.error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.formGroup&lt;/span&gt; &lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.requestForm&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;0.0125rem&lt;/span&gt; &lt;span class="m"&gt;#000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.errorMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;red&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s check out our app’s UI after submitting an empty form again:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AhNk9DMMrOb7zyWNg1e3qQQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AhNk9DMMrOb7zyWNg1e3qQQ.png" alt="App displays error messages in red and keeps the form the same height" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;App displays error messages in red and keeps the form the same height&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Much better! The error messages are shown in red, and the form maintains a consistent height regardless of whether or not error messages appear on the screen.&lt;/p&gt;

&lt;p&gt;As always, we’ll check our tests as well to make sure we didn’t break anything:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3180%2F1%2AlTxiUwNXstM2p8DYGieF5Q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3180%2F1%2AlTxiUwNXstM2p8DYGieF5Q.png" alt="Our tests are still passing after our fourth style refactor" width="800" height="207"&gt;&lt;/a&gt;&lt;em&gt;Our tests are still passing after our fourth style refactor&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Everything is still passing! Excellent. Another successful refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test #7: Confirmation Message on Successful Form Submission
&lt;/h2&gt;

&lt;p&gt;With that, we’re ready for our seventh and final test. We now want to assert that the form can be successfully submitted and that a confirmation screen is shown to the user.&lt;/p&gt;

&lt;p&gt;We’ll write the following test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;replaces the form with a confirmation message when submitted successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RequestForm&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fireEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tyler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tyler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;fireEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Last Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hawkins&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Last Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hawkins&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;fireEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test@test.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test@test.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;fireEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Request Demo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Thank you! We will be in touch with you shortly.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Last Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walking through the test, we first render the form. We then fill out the form, one input at a time, and assert that the value of each input is what we would expect. Then we click the submit button to submit the form. We then assert that the confirmation message is shown on the screen and that the original form no longer appears.&lt;/p&gt;

&lt;p&gt;When we run our tests, this test fails. (Shocker, right?)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3404%2F1%2AvEiET_Nh26TxHDPISfcrUQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3404%2F1%2AvEiET_Nh26TxHDPISfcrUQ.png" alt="Our seventh test fails because the RequestForm component does not yet handle the successful form submission" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;Our seventh test fails because the RequestForm component does not yet handle the successful form submission&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now we can write our source code to implement the successful form submission functionality. We’ll add the confirmation text that is conditionally rendered when a new &lt;code&gt;submitted&lt;/code&gt; piece of state is &lt;code&gt;true&lt;/code&gt;. We’ll also break up our single &lt;code&gt;onChange&lt;/code&gt; handler into separate &lt;code&gt;onChange&lt;/code&gt; handlers just for fun.&lt;/p&gt;

&lt;p&gt;The final &lt;code&gt;RequestForm.js&lt;/code&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./RequestForm.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RequestForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFirstName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLastName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleFirstNameChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setFirstName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleLastNameChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLastName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleEmailChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;firstNameError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFirstNameError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lastNameError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLastNameError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmailError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;submitted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSubmitted&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;setFirstNameError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First Name field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setLastNameError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Last Name field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setEmailError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email field is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setSubmitted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;submitted&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Thank you! We will be in touch with you shortly.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"requestForm"&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`formGroup&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;firstNameError&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;First Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;
          &lt;span class="na"&gt;data-testid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleFirstNameChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;firstNameError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"errorMessage"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;firstNameError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`formGroup&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastNameError&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Last Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;
          &lt;span class="na"&gt;data-testid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lastName"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleLastNameChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastNameError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"errorMessage"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lastNameError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`formGroup&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;data-testid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleEmailChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"errorMessage"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Request Demo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, we now have seven passing tests!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3168%2F1%2AGCVcrVi6rvFx5T1uh7w6IA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3168%2F1%2AGCVcrVi6rvFx5T1uh7w6IA.png" alt="Our seventh test passes now that the form handles successful submission and displaying a confirmation message" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;Our seventh test passes now that the form handles successful submission and displaying a confirmation message&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We can confirm the behavior manually in our app too by filling out the form fields:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AY1lUIQE9KE2gmxu5C-Egcg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AY1lUIQE9KE2gmxu5C-Egcg.png" alt="Filling out the form" width="800" height="556"&gt;&lt;/a&gt;&lt;em&gt;Filling out the form&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We’ll click the submit button and… voilà! A simple confirmation message appears on the screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2A949BaJYtO6TK0sx08RG5Dw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2A949BaJYtO6TK0sx08RG5Dw.png" alt="App displays a confirmation screen after successful form submission" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;App displays a confirmation screen after successful form submission&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We did it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Resulting Code Coverage
&lt;/h2&gt;

&lt;p&gt;We’ve now finished building our “Request Demo” form. We’ve built out all the functionality requested by our product manager and UX designer, and the resulting UI matches the mockups perfectly.&lt;/p&gt;

&lt;p&gt;But how did we do on our test coverage? Were the tests we wrote sufficient? We can generate the test coverage report by running &lt;code&gt;yarn test:coverage&lt;/code&gt;. The output is below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2824%2F0%2AWXXR79Udig9tCub5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2824%2F0%2AWXXR79Udig9tCub5.png" alt="100% code coverage. Nice!" width="800" height="400"&gt;&lt;/a&gt;&lt;em&gt;100% code coverage. Nice!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Amazing! 100% test coverage. This is one of the major benefits of TDD: all of your code, in theory, should be covered by tests because the only source code you write is code to make your tests pass. And because the tests are derived from product requirements, you can be sure that they are testing the right things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So, what have we learned? In the beginning of this article, we were determined to answer three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Can&lt;/em&gt;&lt;/strong&gt; we use TDD to build UIs?&lt;/li&gt;
&lt;li&gt;If so, &lt;strong&gt;&lt;em&gt;how&lt;/em&gt;&lt;/strong&gt; do we do it?&lt;/li&gt;
&lt;li&gt;And finally, &lt;strong&gt;&lt;em&gt;should&lt;/em&gt;&lt;/strong&gt; we use TDD to build UIs?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, &lt;strong&gt;&lt;em&gt;can&lt;/em&gt;&lt;/strong&gt; we use TDD when building UIs? Yes! We absolutely can!&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;&lt;em&gt;how&lt;/em&gt;&lt;/strong&gt; do we do this? By following a process similar to what we’ve outlined by building our demo app. We follow the red, green, refactor cycle and implement style changes during the “refactor” phase.&lt;/p&gt;

&lt;p&gt;Now, &lt;strong&gt;&lt;em&gt;should&lt;/em&gt;&lt;/strong&gt; we use TDD when building UIs? Maybe. Test-driven development isn’t everyone’s cup of tea. But I strongly believe that all frontend developers should at least try it. Give it a shot and see if it works for you. As we’ve demonstrated, using TDD to build UIs isn’t as difficult as you may think. In fact, you may even find that TDD speeds up your development process while ensuring that you test the right things.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>tdd</category>
    </item>
    <item>
      <title>Four strategies for deciding how to decide (plus two more tips)</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Thu, 22 Jan 2026 16:00:00 +0000</pubDate>
      <link>https://forem.com/thawkin3/four-strategies-for-deciding-how-to-decide-plus-two-more-tips-4omm</link>
      <guid>https://forem.com/thawkin3/four-strategies-for-deciding-how-to-decide-plus-two-more-tips-4omm</guid>
      <description>&lt;p&gt;Decision making is hard. Decision making in large organizations is even harder.&lt;/p&gt;

&lt;p&gt;Have you ever tried to lead a large change but struggled to get consensus from the group?&lt;/p&gt;

&lt;p&gt;Or have you ever been in a meeting where you've debated an idea for what feels like the hundredth time but still haven't reached a decision?&lt;/p&gt;

&lt;p&gt;Or have you ever been asked to provide input on a decision, only to be overruled, and then wondered why you were asked in the first place?&lt;/p&gt;

&lt;p&gt;All of these frustrating scenarios are the result of one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Utilizing ineffective decision-making models, or&lt;/li&gt;
&lt;li&gt;Failing to properly communicate the decision-making model being used.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Four decision-making models
&lt;/h2&gt;

&lt;p&gt;So, how do we make decisions more effectively?&lt;/p&gt;

&lt;p&gt;The book &lt;em&gt;Crucial Conversations&lt;/em&gt; offers some excellent advice on decision-making models and outlines four possible strategies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Command.&lt;/strong&gt; Decisions are made without involving others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consult.&lt;/strong&gt; Input is gathered from the group and then a subset decides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vote.&lt;/strong&gt; An agreed-upon percentage swings the decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consensus.&lt;/strong&gt; Everyone comes to an agreement and then supports the final decision.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For large projects where everyone seems to have an opinion, the ideal model is usually &lt;strong&gt;Consult&lt;/strong&gt;, where input is welcome, but someone needs to make the final call. To make this model work, people need to know that this is the decision-making model being used and know who the final decision maker is.&lt;/p&gt;

&lt;p&gt;Where we get into trouble is if people think that this is a &lt;strong&gt;Vote&lt;/strong&gt; or &lt;strong&gt;Consensus&lt;/strong&gt; situation. In the instance of a big initiative that involves dozens of stakeholders, it's a bit of a fool's errand to try to achieve actual consensus and make everyone happy or try to implement every person's feedback.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hard decisions vs. important decisions
&lt;/h2&gt;

&lt;p&gt;Even after choosing a decision-making model and communicating that choice to everyone involved, it's easy to confuse a hard decision with an important decision.&lt;/p&gt;

&lt;p&gt;Choosing what to eat for breakfast can be hard, but it's probably not important. Learning that not every hard decision is also an important decision can greatly relieve your anxiety or analysis paralysis and allow you to just pick something, knowing that your choice probably wasn't that important.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reversible vs. irreversible decisions
&lt;/h2&gt;

&lt;p&gt;Lastly, consider how easy or difficult it will be to change direction after deciding. If the work can easily be undone or changed (a reversible decision), then move forward quickly with what you think the best choice is. No need to overthink things if a wrong choice can be easily corrected.&lt;/p&gt;

&lt;p&gt;Irreversible decisions, those that can't be changed (or at least can't be easily changed) should require more thought. Once an irreversible decision is made, it'll be harder to change course in the future.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Understanding these three principles has greatly improved my decision-making skills throughout my career. Remember:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recognize and communicate the decision-making model being used.&lt;/li&gt;
&lt;li&gt;Don't confuse a hard decision with an important one.&lt;/li&gt;
&lt;li&gt;Make reversible decisions quickly. Make irreversible decisions with care.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>career</category>
      <category>careerdevelopment</category>
      <category>productivity</category>
      <category>growth</category>
    </item>
    <item>
      <title>Beyond Code: Communication Skills Every Software Engineer Needs</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Wed, 21 Jan 2026 07:25:27 +0000</pubDate>
      <link>https://forem.com/thawkin3/beyond-code-communication-skills-every-software-engineer-needs-5714</link>
      <guid>https://forem.com/thawkin3/beyond-code-communication-skills-every-software-engineer-needs-5714</guid>
      <description>&lt;p&gt;Software engineers are not exactly known for their great communication skills. However, despite what you might think, communicating with others is a large part of any software engineering job.&lt;/p&gt;

&lt;p&gt;As a software engineer, you likely spend more time communicating than writing code: asking for help, providing help, coordinating projects, submitting code, reviewing code, writing proposals, reviewing proposals, presenting in meetings, having discussions in meetings, submitting bug reports, responding to bug reports, and more.&lt;/p&gt;

&lt;p&gt;How well you can communicate will influence how well you’re able to work with others — and how you’re perceived.&lt;/p&gt;

&lt;p&gt;Communication is one of those soft skills that will help you progress in your career more than just about anything else, even technical skills. The best software engineers I know are great coders but also excellent at communicating with others.&lt;/p&gt;

&lt;p&gt;In my last decade of software engineering, I’ve seen a fair share of both good and bad communication (sometimes it feels like more bad than good). What follows are some tips for how you can communicate more effectively in your day-to-day work.&lt;/p&gt;




&lt;h2&gt;
  
  
  When creating a pull request
&lt;/h2&gt;

&lt;p&gt;Always write a thorough pull request (PR) description. Include some brief context for what the PR does and why. Add screenshots or videos to show the changes in action. Provide steps for how to test the changes so the reviewer can verify that the code is working as expected.&lt;/p&gt;

&lt;p&gt;As the code author, you show competence by being thorough. You also show that you respect your code reviewer’s time by doing those things that will make it easy for them to review your code.&lt;/p&gt;

&lt;p&gt;Nothing frustrates me more as a reviewer than being asked to review a PR that has absolutely no context — just a blank PR description or maybe a link to a Jira ticket. What if I don’t know what these changes do? What if I don’t know why these changes are being made? What if I don’t know how to test these changes or where I can even find them in the app? This kind of missing information leads to delays as we go back and forth requesting and gathering more details.&lt;/p&gt;

&lt;p&gt;As the code author, you should do as much as possible to reduce the cognitive load on the reviewer. Providing context is just one of many ways you can &lt;a href="https://mtlynch.io/code-review-love/" rel="noopener noreferrer"&gt;make your code reviewer fall in love with you&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  When reviewing code
&lt;/h2&gt;

&lt;p&gt;Be clear in each comment whether your feedback is an optional “nit” (nitpick) or if it’s blocking feedback. When no explicit distinction is made, it’s sometimes hard for the code author to know if you’re requesting changes, just thinking out loud, or providing alternative approaches just as something to consider for the future.&lt;/p&gt;

&lt;p&gt;Be as helpful as you possibly can in your comments. Don’t just say something is “wrong” or needs to be changed. Explain the pitfalls of the current approach and then give a suggested solution if you have one. If the code change is small enough, provide that code snippet in your comment so that the code author can easily apply it. Make it easy for the code author to take your advice.&lt;/p&gt;

&lt;p&gt;Time is wasted when both the code author and code reviewer have to spend multiple cycles trying to get clarity on the importance of the requested changes and what an acceptable solution might look like.&lt;/p&gt;

&lt;p&gt;Finally, when you’ve requested changes and are finished reviewing, clearly tell the code author that you’re done reviewing for now. Ask them to let you know when the changes have been made and the PR is ready for another review. Time is wasted when one or both of you are sitting around incorrectly thinking that you are waiting on the other person.&lt;/p&gt;




&lt;h2&gt;
  
  
  When submitting a bug report
&lt;/h2&gt;

&lt;p&gt;Always include steps to reproduce, the expected behavior, the actual behavior you see, and screenshots or videos.&lt;/p&gt;

&lt;p&gt;You should strive to make it as easy as possible for someone to understand what issue you’re reporting.&lt;/p&gt;

&lt;p&gt;As someone on the receiving end of the bug report, nothing is more frustrating than receiving incomplete information. If all you tell me is “this feature doesn’t work”, I’ll have many followup questions for you: What exactly doesn’t work? Does the app crash? Does it do something but just not what you thought it would do? How are you getting into this state? What are you trying to do? Are there any error messages in the UI or in the console logs?&lt;/p&gt;

&lt;p&gt;Gathering missing information that wasn’t included in the bug report leads to more wasted time and more back and forth.&lt;/p&gt;

&lt;p&gt;Remember that when you’re submitting a bug report, you’re often looking for help in getting this bug fixed. Be considerate of others and try to be as helpful as you can so that they can help you. Think of what information someone solving the problem might need. When in doubt, providing more information is generally better than providing less information.&lt;/p&gt;




&lt;h2&gt;
  
  
  When asking for help
&lt;/h2&gt;

&lt;p&gt;Don’t fall prey to &lt;a href="https://xyproblem.info/" rel="noopener noreferrer"&gt;the XY problem&lt;/a&gt;: asking about your attempted solution rather than your actual problem.&lt;/p&gt;

&lt;p&gt;For those not familiar with the XY problem, the scenario usually goes something like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;User wants to do X.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User doesn’t know how to do X, but thinks they can fumble their way to a solution if they can just manage to do Y.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User doesn’t know how to do Y either.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User asks for help with Y.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Others try to help user with Y, but are confused because Y seems like a strange problem to want to solve.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After much interaction and wasted time, it finally becomes clear that the user really wants help with X, and that Y wasn’t even a suitable solution for X.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://xyproblem.info/" rel="noopener noreferrer"&gt;The XY Problem&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, take a step back and provide context when asking for help. What are you actually trying to solve and why? Then, what have you tried, and what was the result of those explorations?&lt;/p&gt;

&lt;p&gt;Without providing the full context for your question, you risk getting help and advice for an attempted solution that won’t actually solve the root of your problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  When requesting that people do something
&lt;/h2&gt;

&lt;p&gt;Be clear what action is required, by whom, by when.&lt;/p&gt;

&lt;p&gt;Meetings can fail for a variety of reasons, particularly if the meeting participants aren’t aligned on what the purpose of the meeting is and what the expected outcome of the meeting should be. If a meeting is coming to a close but it’s unclear what the next steps are, don’t be afraid to be the one who asks “Okay, what are we doing next as a result of this discussion?”&lt;/p&gt;

&lt;p&gt;Meetings also become unproductive when notes are taken but the action items are vague. (Or worse, notes aren’t taken at all!) If it’s not clear who owns each action item, the work most likely won’t be done. Instead, assign an owner by writing someone’s name next to each bullet point, and follow up during the next meeting or agreed upon deadline.&lt;/p&gt;

&lt;p&gt;Similarly, when sending out announcements over Slack or email, be clear in your message if the information is just an FYI or if action is required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Everything outlined above is just a small handful of specific scenarios in which you might find yourself. There are sure to be others!&lt;/p&gt;

&lt;p&gt;In all your communication, aim for respect and clarity, and try to find ways to reduce the cognitive load for the other person. Strive not just to be understood, but to be so clear that it is impossible to be misunderstood.&lt;/p&gt;

</description>
      <category>career</category>
      <category>programming</category>
      <category>productivity</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Lessons from The Clean Coder</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Wed, 26 Feb 2025 13:39:31 +0000</pubDate>
      <link>https://forem.com/thawkin3/lessons-from-the-clean-coder-2l9p</link>
      <guid>https://forem.com/thawkin3/lessons-from-the-clean-coder-2l9p</guid>
      <description>&lt;p&gt;I read &lt;em&gt;The Clean Coder&lt;/em&gt; by Robert C. Martin back in 2022, and I remember feeling like it was chock-full of wisdom.&lt;/p&gt;

&lt;p&gt;To avoid confusion, this is not the book &lt;em&gt;Clean Code&lt;/em&gt; by the same author. &lt;em&gt;Clean Code&lt;/em&gt; focuses mostly on coding itself and hard skills for writing good code. &lt;em&gt;The Clean Coder&lt;/em&gt; is a followup book and focuses on soft skills — how we as developers can become true professionals and craftsmen.&lt;/p&gt;

&lt;p&gt;Some of the advice in this book is controversial. The piece that stands out most to me is the statement that developers should be spending 60 hours per week on their craft — 40 hours at work and 20 hours learning outside of work. That may not be realistic for many developers, especially those with obligations to their family, community, or elsewhere. The underlying sentiment is useful though, which is that the best developers invest in themselves and are continuously learning. It’s up to you to decide what that looks like in your life.&lt;/p&gt;

&lt;p&gt;When I read tech books, I always take notes and highlight passages that resonate with me. It helps me retain the information I found meaningful. I’ve reproduced most of my notes below, and I hope that you’ll find something that resonates with you as well.&lt;/p&gt;

&lt;p&gt;Enjoy!&lt;/p&gt;

&lt;h2&gt;
  
  
  Preface
&lt;/h2&gt;

&lt;p&gt;Managers should not overrule the expert opinions of those they lead, especially when their direct reports have thought about the problem longer, have more context, and have more relevant experience and expertise in the area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 1: Professionalism
&lt;/h2&gt;

&lt;p&gt;“You cannot simply keep making the same errors over and over. As you mature in your profession, your error rate should rapidly decrease towards the asymptote of zero. It won’t ever get to zero, but it is your responsibility to get as close as possible to it.”&lt;/p&gt;

&lt;p&gt;“It is unprofessional in the extreme to purposely send code that you know to be faulty to QA.”&lt;/p&gt;

&lt;p&gt;“Every single line of code that you write should be tested. Period.”&lt;/p&gt;

&lt;p&gt;“Why do most developers fear to make continuous changes to their code? They are afraid they’ll break it. Why are they afraid they’ll break it? Because they don’t have tests. It all comes back to the tests.”&lt;/p&gt;

&lt;p&gt;“Your career is your responsibility. It is not your employer’s responsibility to make sure you are marketable. It is not your employer’s responsibility to train you, or to send you to conferences, or to buy you books. These things are your responsibility. Woe to the software developer who entrusts his career to his employer.” (He then goes on to say that some employers do provide training or conferences or learning budgets for you, and you should be grateful when they do.)&lt;/p&gt;

&lt;p&gt;“You should plan on working 60 hours per week. The first 40 are for your employer. The remaining 20 are for you. During this remaining 20 hours you should be reading, practicing, learning, and otherwise enhancing your career. … Perhaps you think that work should stay at work and that you shouldn’t bring it home. I agree! You should not be working for your employer during those 20 hours. Instead, you should be working on your career. … During that 20 hours you should be doing those things that reinforce that passion. Those 20 hours should be fun!”&lt;/p&gt;

&lt;p&gt;At a minimum, every software professional should be familiar with design patterns (gang of four), design principles (SOLID), methods (Waterfall, Agile, Scrum, Kanban), disciplines (TDD, OOP, pair programming, CI/CD), and artifacts (UML, flow charts).&lt;/p&gt;

&lt;p&gt;“The best way to learn is to teach.”&lt;/p&gt;

&lt;p&gt;“It is the responsibility of every software professional to understand the domain of the solutions they are programming. … It is the worst kind of unprofessional behavior to simply code from a spec without understanding why that spec makes sense to the business. Rather, you should know enough about the domain to be able to recognize and challenge specification errors.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 2: Saying No
&lt;/h2&gt;

&lt;p&gt;“Professionals speak truth to power. Professionals have the courage to say no to their managers.”&lt;/p&gt;

&lt;p&gt;“A team player is not someone who says yes all the time.”&lt;/p&gt;

&lt;p&gt;Stick to your estimates if you have reason to believe they are accurate, even if you are under pressure to commit to finishing at an earlier date. Estimates, targets, and commitments are three different things. Unless you have a radical plan to change how you work, your priorities, or the scope of the work, committing to an earlier deadline than you believe you can finish in is unprofessional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 3: Saying Yes
&lt;/h2&gt;

&lt;p&gt;“There are three parts to making a commitment. 1) You say you’ll do it. 2) You mean it. 3) You actually do it.”&lt;/p&gt;

&lt;p&gt;“There are very few people who, when they say something, they mean it and then actually get it done. There are some who will say things and mean them, but they never get it done. And there are far more people who promise things and don’t even mean to do them.”&lt;/p&gt;

&lt;p&gt;“You can only commit to things that you have full control of. For example, if your goal is to finish a module that also depends on another team, you can’t commit to finish the module with full integration with the other team. But you can commit to specific actions that will bring you to your target.”&lt;/p&gt;

&lt;p&gt;“If you can’t make your commitment, the most important thing is to raise a red flag as soon as possible to whoever you committed to.”&lt;/p&gt;

&lt;p&gt;“Professionals are not required to say yes to everything that is asked of them. However, they should work hard to find creative ways to make ‘yes’ possible.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 4: Coding
&lt;/h2&gt;

&lt;p&gt;Being able to sense your errors and having a fast feedback loop is extremely important.&lt;/p&gt;

&lt;p&gt;“Often the customer’s requirements do not actually solve the customer’s problems. It is up to you to see this and negotiate with the customer to ensure that the customer’s true needs are met.”&lt;/p&gt;

&lt;p&gt;It’s difficult to write code when you’re worried about something else outside of work, like problems at home or a fight with your spouse. Don’t try to code during these times. Seek instead to resolve the problem so your worries go away. Then you’ll be able to focus again.&lt;/p&gt;

&lt;p&gt;“It is incumbent upon you as a professional to reduce your debugging time as close to zero as you can get. Clearly zero is an asymptotic goal, but it is the goal nonetheless.”&lt;/p&gt;

&lt;p&gt;“I have solved an inordinate number of problems in the shower. … Sometimes the best way to solve a problem is to go home, eat dinner, watch TV, go to bed, and then wake up the next morning and take a shower.”&lt;/p&gt;

&lt;p&gt;When working on a project or task, “regularly measure your progress against your goal, and come up with three fact-based end dates: best case, nominal case, and worst case.”&lt;/p&gt;

&lt;p&gt;“Of all the unprofessional behaviors that a programmer can indulge in, perhaps the worst of all is saying you are done when you know you aren’t.”&lt;/p&gt;

&lt;p&gt;“You avoid the problem of false delivery by creating an independent definition of ‘done.’”&lt;/p&gt;

&lt;p&gt;“Learn how to ask for help. When you are stuck, or befuddled, or just can’t wrap your mind around a problem, ask someone for help. … It is unprofessional to remain stuck when help is easily accessible.”&lt;/p&gt;

&lt;p&gt;“Nothing can bring a young software developer to high performance quicker than his own drive, and effective mentoring by his seniors.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 5: Test Driven Development
&lt;/h2&gt;

&lt;p&gt;“The bottom line is that TDD works, and everybody needs to get over it.”&lt;/p&gt;

&lt;p&gt;Follow the “red, green, refactor” cycle.&lt;/p&gt;

&lt;p&gt;“When you have a suite of tests that you trust, then you lose all fear of making changes.”&lt;/p&gt;

&lt;p&gt;“Writing your tests first creates a force that drives you to a better decoupled design.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 6: Practicing
&lt;/h2&gt;

&lt;p&gt;“The purpose of learning a kata [in martial arts] is not to perform it on stage. The purpose is to train your mind and body how to react in a particular combat situation. The goal is to make the perfected movements automatic and instinctive so that they are there when you need them.”&lt;/p&gt;

&lt;p&gt;“[Programming katas are] a good way to drive common problem/solution pairs into your subconscious, so that you simply know how to solve them when facing them in real programming.”&lt;/p&gt;

&lt;p&gt;“Practicing is what you do when you aren’t getting paid. You do it so that you will be paid, and paid well.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 7: Acceptance Testing
&lt;/h2&gt;

&lt;p&gt;On changing requirements: “The problem is that things appear different on paper than they do in a working system. When the business actually sees what they specified running in a system, they realize that it wasn’t what they wanted at all. Once they see the requirement actually running, they have a better idea of what they really want and it’s usually not what they are seeing.”&lt;/p&gt;

&lt;p&gt;“The purpose of acceptance tests is communication, clarity, and precision. By agreeing to them, the developers, stakeholders, and testers all understand what the plan for the system behavior is.”&lt;/p&gt;

&lt;p&gt;“Acceptance tests should always be automated. There is a place for manual testing elsewhere in the software lifecycle, but these kinds of tests should never be manual. The reason is simple: cost. … The cost of automating acceptance tests is so small in comparison to the cost of executing manual test plans that it makes no economic sense to write scripts for humans to execute.”&lt;/p&gt;

&lt;p&gt;“Typical business analysts write the ‘happy path’ versions of the tests, because those tests describe the features that have business value. QA typically writes the ‘unhappy path’ tests, the boundary conditions, exceptions, and corner cases. This is because QA’s job is to help think about what can go wrong.”&lt;/p&gt;

&lt;p&gt;“Acceptance tests are not unit tests. Unit tests are written by programmers for programmers. They are formal design documents that describe the lowest level structure and behavior of the code. The audience is programmers, not business. Acceptance tests are written by the business for the business (even when you, the developer, end up writing them). They are formal requirements documents that specify how the system should behave from the business’ point of view. The audience is the business and the programmers.”&lt;/p&gt;

&lt;p&gt;“Unit tests and acceptance tests are documents first, and tests second. Their primary purpose is to formally document the design, structure, and behavior of the system. The fact that they automatically verify the design, structure, and behavior that they specify is wildly useful, but the specification is their true purpose.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 8: Testing Strategies
&lt;/h2&gt;

&lt;p&gt;“Professional developers test their code.”&lt;/p&gt;

&lt;p&gt;Follow the testing pyramid.&lt;/p&gt;

&lt;p&gt;“Unit tests provide as close to 100% coverage as is practical. Generally this number should be somewhere in the 90s. And it should be true coverage as opposed to false tests that execute code without asserting the behavior.”&lt;/p&gt;

&lt;p&gt;“Component tests cover roughly half the system. They are directed more towards happy-path situations and very obvious corner, boundary, and alternate-path cases. The vast majority of unhappy-path cases are covered by unit tests and are meaningless at the level of component tests.”&lt;/p&gt;

&lt;p&gt;“Integration tests are typically not executed as part of the Continuous Integration suite, because they often have longer runtimes. Instead, these tests are run periodically (nightly, weekly, etc.) as deemed necessary by their authors.”&lt;/p&gt;

&lt;p&gt;“System tests cover perhaps 10% of the system. This is because their intent is not to ensure correct system behavior, but correct system construction. The correct behavior of the underlying code and components have already been ascertained by the lower layers of the pyramid.”&lt;/p&gt;

&lt;p&gt;“[Manual exploratory tests] are not automated, nor are they scripted. … Creating a written test plan for this kind of testing defeats the purpose.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 9: Time Management
&lt;/h2&gt;

&lt;p&gt;“Sometimes a meeting will be about something that interests you, but is not immediately necessary. You will have to choose whether you can afford the time. Be careful — there may be more than enough of these meetings to consume your days.”&lt;/p&gt;

&lt;p&gt;“Sometimes you find yourself siting in a meeting that you would have declined had you known more. … Remaining in a meeting that has become a waste of time for you, and to which you can no longer significantly contribute, is unprofessional.”&lt;/p&gt;

&lt;p&gt;“Iteration planning meetings are meant to select the backlog items that will be executed in the next iteration. Estimates should already be done for the candidate items. Assessment of business value should already be done. In really good organizations the acceptance/component tests will already be written, or at least sketched out.”&lt;/p&gt;

&lt;p&gt;You can only intensely focus for so long. Manage your time wisely so that you are coding during high-focus times and doing more trivial tasks during other times.&lt;/p&gt;

&lt;p&gt;“[Professionals] never become so vested in a solution that they can’t abandon it.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 10: Estimation
&lt;/h2&gt;

&lt;p&gt;“Business likes to view estimates as commitments. Developers like to view estimates as guesses. The difference is profound.”&lt;/p&gt;

&lt;p&gt;“An estimate is not a number. An estimate is a distribution.”&lt;/p&gt;

&lt;p&gt;“Professionals draw a clear distinction between estimates and commitments. They do not commit unless they know for certain they will succeed.”&lt;/p&gt;

&lt;p&gt;PERT (Program Evaluation and Review Technique): You provide three numbers for your estimate — optimistic, nominal (most likely), and pessimistic.&lt;/p&gt;

&lt;p&gt;“Software professionals are very careful to set reasonable expectations despite the pressure to try to go fast.”&lt;/p&gt;

&lt;p&gt;“[When estimating tasks as a team,] agreement does not need to be absolute. As long as the estimates are close, it’s good enough. So, for example, a smattering of 3s and 4s is agreement. However if everyone holds up 4 fingers except for one person who holds up 1 finger, then they have something to talk about.”&lt;/p&gt;

&lt;p&gt;“The simultaneity of displaying fingers is important. We don’t want people changing their estimates based on what they see other people do.”&lt;/p&gt;

&lt;p&gt;“If you break up a large task into many smaller tasks and estimate them independently, the sum of the estimates of the small tasks will be more accurate than a single estimate of the larger task.”&lt;/p&gt;

&lt;p&gt;“Errors in estimates tend toward underestimation and not overestimation.”&lt;/p&gt;

&lt;p&gt;“Breaking the tasks up is a good way to understand those tasks better and uncover surprises.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 11: Pressure
&lt;/h2&gt;

&lt;p&gt;“You know what you believe by observing yourself in a crisis. If in a crisis you follow your disciplines, then you truly believe in those disciplines. On the other hand, if you change your behavior in a crisis, then you don’t truly believe in your normal behavior.”&lt;/p&gt;

&lt;p&gt;“Slow down. Think the problem through.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 12: Collaboration
&lt;/h2&gt;

&lt;p&gt;“We didn’t become programmers because we like working with people.”&lt;/p&gt;

&lt;p&gt;“It is far better to break down all walls of code ownership and have the team own all the code. I prefer teams in which any team member can check out any module and make any changes they think are appropriate. I want the team to own the code, not the individuals.”&lt;/p&gt;

&lt;p&gt;“Although all team members have a position to play, all team members should also be able to play another position in a pinch.”&lt;/p&gt;

&lt;p&gt;“Perhaps you believe that you work better when you work alone. That may be true, but it doesn’t mean that the team works better when you work alone. … There are times when working alone is the right thing to do. There are times when you simply need to think long and hard about a problem. There are times when the task is so trivial that it would be a waste to have another person working with you. But, in general, it is best to collaborate closely with others and to pair with them a large fraction of the time.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 13: Teams and Projects
&lt;/h2&gt;

&lt;p&gt;“It makes no sense to tell a programmer to devote half their time to project A and the rest of their time to project B, especially when the two projects have two different project managers, different business analysts, different programmers, and different testers.”&lt;/p&gt;

&lt;p&gt;“It takes time for a team to work out their differences, come to terms with each other, and really gel. It might take six months. It might even take a year. But once it happens, it’s magic. A gelled team will plan together, solve problems together, face issues together, and get things done. Once this happens, it’s ludicrous to break it apart just because a project comes to an end. It’s best to keep that team together and just keep feeding it projects.”&lt;/p&gt;

&lt;p&gt;“Teams are harder to build than projects. Therefore, it is better to form persistent teams that move together from one project to the next and can take on more than one project at a time. The goal in forming a team is to give that team enough time to gel, and then keep it together as an engine for getting many projects done.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 14: Mentoring, Apprenticeship, and Craftsmanship
&lt;/h2&gt;

&lt;p&gt;“I’e noticed that [software engineers who do really well] have something in common: Nearly all of them taught themselves to program before they entered university and continued to teach themselves despite university.”&lt;/p&gt;

&lt;p&gt;“Even the best CS degree programs do not typically prepare the young graduate for what they will find in industry. … What you learn in school and what you find on the job are often very different things.”&lt;/p&gt;

&lt;p&gt;“Companies lose huge amounts of money due to the inadequate training of their software developers. Somehow the software development industry has gotten the idea that programmers are programmers, and that once you graduate you can code. Indeed, it is not at all uncommon for companies to hire kids right out of school, form them into ‘teams,’ and ask them to build the most critical systems. It’s insane!”&lt;/p&gt;

&lt;p&gt;“A craftsman is someone who works quickly, but without rushing, who provides reasonable estimates and meets commitments. A craftsman knows when to say no, but tries hard to say yes. A craftsman is a professional. Craftsmanship is the mindset held by craftsmen. Craftsmanship is a meme that contains values, disciplines, techniques, attitudes, and answers.”&lt;/p&gt;

&lt;p&gt;“School can teach the theory of computer programming. But school does not, and cannot teach the discipline, practice, and skill of being a craftsman. Those things are acquired through years of personal tutelage and mentoring. It is time for those of us in the software industry to face the fact that guiding the next batch of software developers to maturity will fall to us, not to the universities. It’s time for us to adopt a program of apprenticeship, internship, and long-term guidance.”&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>cleancode</category>
      <category>learning</category>
    </item>
    <item>
      <title>The Value of Finishing</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Mon, 03 Feb 2025 15:23:14 +0000</pubDate>
      <link>https://forem.com/thawkin3/the-value-of-finishing-44o</link>
      <guid>https://forem.com/thawkin3/the-value-of-finishing-44o</guid>
      <description>&lt;p&gt;“The value of the thing is found in finishing the thing.”&lt;/p&gt;

&lt;p&gt;This is the advice I like to give about most software engineering projects. You can keep working on something indefinitely to get it &lt;em&gt;just right&lt;/em&gt;, but there’s no value provided until you release it and it’s actually in the hands of customers who can use it.&lt;/p&gt;

&lt;p&gt;Until that pull request is merged and the code is deployed to production, it’s not real. It’s just an idea.&lt;/p&gt;

&lt;p&gt;Something that is released and that is imperfect is more valuable than a thing that’s unreleased and is completely perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real world examples
&lt;/h2&gt;

&lt;p&gt;Let’s consider some analogies:&lt;/p&gt;

&lt;p&gt;There is no value in assembling half of a piece of furniture. It’s not until the furniture is completely assembled that you can use it.&lt;/p&gt;

&lt;p&gt;There is no value in putting your clothes in the washer but not the dryer. It’s not until the clothes are both washed and dried that they’re ready to be worn again.&lt;/p&gt;

&lt;p&gt;There is no value in cooking a meal but not eating it. Eating and enjoying the food is the point of making the meal.&lt;/p&gt;

&lt;p&gt;There is no value in writing a blog post draft but not hitting the Submit button. You could spend your whole life editing and revising your words, but it’s not until your work is published that other people can read it and benefit from it.&lt;/p&gt;

&lt;p&gt;The unfinished task provides no value. In fact, the unfinished task is usually just a mess. It’s finishing the task that provides the value. Getting to the end result is the reason you even started the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for software engineering
&lt;/h2&gt;

&lt;p&gt;So what does this idea mean for software engineers?&lt;/p&gt;

&lt;p&gt;It means that you should complete work in small chunks that you can release. Don’t release sloppy or unfinished work, but release things that solve real problems and provide real value, even if they don’t solve every problem &lt;em&gt;yet&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It means you should merge and deploy your code often. Avoid long-running feature branches.&lt;/p&gt;

&lt;p&gt;It means you shouldn’t let “perfect” become the enemy of the good. Get the thing out there, even if it doesn’t have the perfect API or the perfect design. Again, don’t cut major corners or create an inferior user experience. But learn what’s good enough, and don’t let perfectionism get in the way.&lt;/p&gt;

&lt;p&gt;It means that working on a single task and finishing it is better than working on three tasks simultaneously and finishing none of them. The one finished thing brings value. The three unfinished things don’t bring any value yet, because they’re not done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Please don’t misunderstand
&lt;/h2&gt;

&lt;p&gt;Don't try to take the lesson too far. I'm not advocating for sloppy work, cutting corners, or lowering your standards. I'm also not saying to focus only on the end result or to ignore the lessons you can learn along the way, especially for projects that ultimately get abandoned. Not every adventure we embark on ends up being a success.&lt;/p&gt;

&lt;p&gt;But I am advocating for finishing.&lt;/p&gt;

&lt;p&gt;The value of the thing is found in finishing the thing.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>career</category>
    </item>
    <item>
      <title>Lessons from A Philosophy of Software Design</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Wed, 15 Jan 2025 15:05:00 +0000</pubDate>
      <link>https://forem.com/thawkin3/lessons-from-a-philosophy-of-software-design-4cn7</link>
      <guid>https://forem.com/thawkin3/lessons-from-a-philosophy-of-software-design-4cn7</guid>
      <description>&lt;p&gt;I recently finished reading &lt;em&gt;A Philosophy of Software Design&lt;/em&gt; by John Ousterhout. If you enjoyed &lt;em&gt;Clean Code&lt;/em&gt; or &lt;em&gt;The Pragmatic Programmer&lt;/em&gt;, you’ll likely enjoy this one too. It’s very similar to those books, has some overlapping content, and focuses primarily on what separates good software design from bad software design.&lt;/p&gt;

&lt;p&gt;When I read tech books, I always take notes and highlight passages that resonate with me. It helps me retain the information I found meaningful. I’ve reproduced most of my notes below, and I hope that you’ll find something that resonates with you as well.&lt;/p&gt;

&lt;p&gt;Enjoy!&lt;/p&gt;

&lt;h2&gt;
  
  
  On coding advice
&lt;/h2&gt;

&lt;p&gt;When you disagree with someone’s coding advice (something in this book or in &lt;em&gt;Clean Code&lt;/em&gt;), try to understand why you disagree with it.&lt;/p&gt;

&lt;p&gt;“Every rule has its exceptions, and every principle has its limits. If you take any design idea to its extreme, you’ll probably end up in a bad place.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Complexity
&lt;/h2&gt;

&lt;p&gt;“Complexity is more apparent to readers than writers. If you write a piece of code and it seems simple to you, but other people think it is complex, then it is complex. When you find yourself in situations like this, it’s worth probing other developers to find out why the code seems complex to them; there are probably some interesting lessons to learn from the disconnect between your opinion and theirs. Your job as a developer is not just to create code that you can work with easily, but to create code that others can also work with easily.”&lt;/p&gt;

&lt;p&gt;Symptoms of complexity&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change amplification&lt;/strong&gt;: Seemingly simple changes require code modifications in many different places&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cognitive load&lt;/strong&gt;: How much a developer needs to know in order to complete a task&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown unknowns&lt;/strong&gt;: It’s not obvious which pieces of code must be modified to complete a task&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good design should be obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modules, interfaces, and implementations
&lt;/h2&gt;

&lt;p&gt;Modules should be deep. Complex implementations should be hidden behind simple interfaces with sensible defaults that work for the most common use cases.&lt;/p&gt;

&lt;p&gt;“The best features are the ones you get without even knowing they exist.”&lt;/p&gt;

&lt;p&gt;“As a module developer, you should strive to make life as easy as possible for the users of your module, even if it means extra work for you.”&lt;/p&gt;

&lt;p&gt;“Reduce the number of places where exceptions must be handled.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Design approaches
&lt;/h2&gt;

&lt;p&gt;“Designing software is hard, so it’s unlikely that your first thoughts about how to structure a module or system will produce the best design. You’ll end up with a much better result if you consider multiple options for each major design decision.”&lt;/p&gt;

&lt;p&gt;“Try to pick approaches that are radically different from each other; you’ll learn more that way. Even if you are certain that there is only one reasonable approach, consider a second design anyway, no matter how bad you think it will be. It will be instructive to think about the weaknesses of that design and contrast them with the features of other designs.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Comments and documentation
&lt;/h2&gt;

&lt;p&gt;Comments should be used to explain things that can’t be expressed in code, like the rationale for why a certain decision was made.&lt;/p&gt;

&lt;p&gt;“The biggest challenge with cross-module documentation is finding a place to put it where it will naturally be discovered by developers.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Consistency, conventions, and coding styles
&lt;/h2&gt;

&lt;p&gt;“Consistency creates cognitive leverage: once you have learned how something is done in one place, you can use that knowledge to immediately understand other places that use that same approach.”&lt;/p&gt;

&lt;p&gt;Document coding conventions and coding style preferences. Automate and enforce what you can, like with ESLint and Prettier. When joining a new company or team, stick to existing conventions that are already in place. Refactoring or re-writing all the code to use a new convention is rarely worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tactical vs. strategic programming; also Agile
&lt;/h2&gt;

&lt;p&gt;Tactical vs. strategic programming. Don’t just focus on getting a small change working if it makes future changes more difficult.&lt;/p&gt;

&lt;p&gt;“One of the risks of agile development is that it can lead to tactical programming. Agile development tends to focus developers on features, not abstractions, and it encourages developers to put off design decisions in order to produce working software as soon as possible. For example, some agile practitioners argue that you shouldn’t implement general-purpose mechanisms right away; implement a minimal special-purpose mechanism to start with, and refactor into something more generic later, once you know that it’s needed. Although these arguments make sense to a degree, they are against an investment approach, and they encourage a more tactical style of programming. This can result in rapid accumulation of complexity.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Importance of tests
&lt;/h2&gt;

&lt;p&gt;“Tests, particularly unit tests, play an important role in software design because they facilitate refactoring. Without a test suite, it’s dangerous to make major structural changes to a system. There’s no easy way to find bugs, so it’s likely that bugs will go undetected until the new code is deployed, where they are much more expensive to find and fix. As a result, developers avoid refactoring in systems without good test suites; they try and minimize the number of code changes for each new feature or bug fix, which means that complexity accumulates and design mistakes don’t get corrected.&lt;/p&gt;

&lt;p&gt;“With a good set of tests, developers can be more confident when refactoring because the test suite will find most bugs that are introduced. This encourages developers to make structural improvements to a system, which results in a better design. Unit tests are particularly valuable: they provide a higher degree of code coverage than system tests, so they are more likely to uncover any bugs.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance improvements
&lt;/h2&gt;

&lt;p&gt;Before making performance changes, measure what you need in order to create benchmarks for the existing performance. Our intuitions of what changes will improve performance are sometimes wrong, having either no meaningful impact or actually making performance worse.&lt;/p&gt;

&lt;p&gt;Design code around the critical path. Performance improvements in other places that are not the biggest bottleneck will still result in a bottleneck when you hit that part of the path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and the benefits of good design
&lt;/h2&gt;

&lt;p&gt;“The investments you make in good design will pay off quickly. The modules you defined carefully at the beginning of a project will save you time later as you reuse them over and over. The clear documentation that you wrote six months ago will save you time when you return to the code to add a new feature. The time you spent honing your design skills will also ay for itself: as your skills and experience grow, you will find that you can produce good designs more and more quickly. Good design doesn’t really take that much longer than quick-and-dirty design, once you know how.&lt;/p&gt;

&lt;p&gt;“The reward for being a good designer is that you get to spend a larger fraction of your time in the design phase, which is fun. Poor designers spend most of their time chasing bugs in complicated and brittle code. If you improve your design skills, not only will you produce higher quality software more quickly, but the software development process will be more enjoyable.”&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>books</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Productivity and Organization Tips for Software Engineers</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Mon, 06 Jan 2025 16:00:00 +0000</pubDate>
      <link>https://forem.com/thawkin3/productivity-and-organization-tips-for-software-engineers-31ij</link>
      <guid>https://forem.com/thawkin3/productivity-and-organization-tips-for-software-engineers-31ij</guid>
      <description>&lt;p&gt;I’ve been a software engineer for a little over a decade now, and I like to think I’m a pretty organized person. I have a system for everything, and these systems help my mind and my day run more smoothly.&lt;/p&gt;

&lt;p&gt;Organization isn’t something that comes naturally to everyone, so today I thought I’d share some of my strategies that help me have a productive and fulfilling work day.&lt;/p&gt;

&lt;p&gt;I’ve organized them below sequentially, walking you through how I start my day, things I do throughout my day, and how I end my day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start of my day
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Startup tasks and message checking
&lt;/h3&gt;

&lt;p&gt;To get oriented each morning, I check several things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My calendar&lt;/li&gt;
&lt;li&gt;My todo list&lt;/li&gt;
&lt;li&gt;Slack&lt;/li&gt;
&lt;li&gt;Email&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This usually takes just 5–10 minutes and helps me get ready for everything going on that day.&lt;/p&gt;

&lt;p&gt;If I have an interview to conduct, I’ll block off time on my calendar before the interview to prepare and after the interview to submit my feedback. If I have a 1on1 with my manager, I’ll add an item to my todo list to prepare notes for what I want to talk about.&lt;/p&gt;

&lt;p&gt;If I have emails or Slack messages that need my attention, I’ll either respond to them right away or add an item to my todo list. As a rule of thumb, if the message only takes a couple minutes to respond to or take care of, I’ll just do it right away. If it’s something that will take longer, like someone asking me to review a pull request or a tech spec, I’ll add that to my todo list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Good morning and morning report
&lt;/h3&gt;

&lt;p&gt;Next, I go through a small routine of morning tasks. This involves saying good morning to my teammates over Slack (we’re all remote) and sending out a short morning report of noteworthy things going on this day.&lt;/p&gt;

&lt;p&gt;The morning report usually includes our sprint goals, pull requests and tech specs needing review, and any relevant upcoming events or action items. The morning report only takes a couple minutes to write, and it helps keep everyone on the team on the same page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessibility tip of the day
&lt;/h3&gt;

&lt;p&gt;I’m passionate about web accessibility, so each morning I also send out a short “Accessibility Tip of the Day” in Slack. This tip of the day is a short tidbit of info, usually focused on engineering, product, and design. I’ve been doing this for about a year and a half now, and I’ve written 300 tips so far! (You can find them on LinkedIn under the hashtag &lt;a href="https://www.linkedin.com/feed/hashtag/?keywords=accessibilitytipoftheday" rel="noopener noreferrer"&gt;#accessibilityTipOfTheDay&lt;/a&gt;.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Urgent tasks and unblocking others
&lt;/h3&gt;

&lt;p&gt;At this point I’m usually about a half hour into my day. If there are any tasks that urgently need to be done, and they can be done quickly, I’ll try to knock out several small things in the next half hour. This usually includes short pull request reviews. I always appreciate people quickly reviewing my code, so I try to do the same. This helps unblock other engineers who are waiting on a review, and it helps keep the work moving along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Throughout the day
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One big impactful thing
&lt;/h3&gt;

&lt;p&gt;What the rest of my day looks like will vary based on how many meetings I have or if I have an interview to conduct, but on my todo list I always have one big thing that’s my main goal for the day. If I can get this one thing done, I’ll consider it a successful day. This could be something like completing an important Jira task, writing an RFC, or finishing a blog post draft for our engineering blog. Whatever the task is, it’s usually something that I need 2–3 hours of uninterrupted time to complete.&lt;/p&gt;

&lt;p&gt;This “one big thing” strategy has a lot of different names, and you may be familiar with ideas like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Paul Graham’s essay “&lt;a href="https://paulgraham.com/makersschedule.html" rel="noopener noreferrer"&gt;Maker’s Schedule, Manager’s Schedule&lt;/a&gt;”, where he argues that software engineers (“makers”) need about a half day of uninterrupted time to get any meaningful work done&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Brian Tracy’s book &lt;em&gt;&lt;a href="https://www.amazon.com/Eat-That-Frog-Great-Procrastinating/dp/162656941X" rel="noopener noreferrer"&gt;Eat That Frog!&lt;/a&gt;&lt;/em&gt;, where he encourages you to do the hardest thing in your day first&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mihaly Csikszentmihalyi’s book &lt;em&gt;&lt;a href="https://www.amazon.com/Flow-Psychology-Experience-Perennial-Classics/dp/0061339202" rel="noopener noreferrer"&gt;Flow&lt;/a&gt;&lt;/em&gt;, which describes a “flow” state of intense enjoyment, creativity, and/or productivity in which you lose yourself in what you’re doing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Oliver Burkeman’s 3–3–3 method, in which he advocates for spending three hours on an important task, doing three smaller tasks, and doing three maintenance tasks each day&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The rocks and sand in a jar analogy, which teaches that you should focus on the big important things first (If you have large rocks, small rocks, sand, and a jar, the order in which you put the items in the jar matters. If you put the sand and small rocks in the jar first, you’ll find that the large rocks don’t fit. But if you put the large rocks in first, then the small rocks, and next the sand, you’ll find that there’s room for all of them. Prioritize the big important things, and there will be room for the rest.)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Several smaller things
&lt;/h3&gt;

&lt;p&gt;I dislike context switching, so after I’ve finished my one big thing, I’ll do a batch of smaller things all in a row. This could be reviewing more pull requests, writing or improving a wiki, reviewing a tech spec, responding to new messages, completing a shorter Jira task, or reading a short blog post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Note taking
&lt;/h3&gt;

&lt;p&gt;I learn best through written communication. I’d much rather read something than watch a video or have a meeting, and I’m much better at organizing my thoughts when I write them down.&lt;/p&gt;

&lt;p&gt;For just about any task I work on, I open a scratch pad in my Notes app to jot down my thoughts. When working on an engineering task, I might write down bullet points of what the problem is and how I’m planning on solving it. When troubleshooting something, I’ll write down the steps I took and what did or didn’t work. This helps me work through problems and also makes it really easy to send my notes to other engineers if I need help.&lt;/p&gt;

&lt;p&gt;This written log usually isn’t something that I ever need to look at again after I’ve finished the task, but it does sometimes come in handy when I encounter a similar problem in the future and want to see how I solved it in the past.&lt;/p&gt;

&lt;h3&gt;
  
  
  Todo lists
&lt;/h3&gt;

&lt;p&gt;I’ve mentioned my todo list already that I create and review each morning. Throughout the day, if a thought pops into my head for something I should do, I add it to my todo list right away. This allows me to go back to whatever I’m actively working on without needing to worry about remembering this other new thing.&lt;/p&gt;

&lt;p&gt;I’ve found that the more information I can get out of my head and written down, the less cognitive load I have, and the less I need to remember.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategic timing
&lt;/h3&gt;

&lt;p&gt;You can get a lot more done in a day if you do things in the “right” order. For example, if I know that I have two hours of meetings in the afternoon, I try to get a pull request ready before then. That way, someone can review my code while I’m in meetings, and (hopefully) my pull request will be ready to be merged as soon as I get out of my last meeting.&lt;/p&gt;

&lt;p&gt;Similarly, if I can get something up for review in the morning, that leaves time for me to switch to other smaller tasks while I wait for a review (the rocks and sand in a jar analogy).&lt;/p&gt;

&lt;h2&gt;
  
  
  End of the day
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Shutdown routine
&lt;/h3&gt;

&lt;p&gt;I end my work day in much the same way that I start it. Before signing off, I review my calendar for tomorrow, and I add items to tomorrow’s todo list.&lt;/p&gt;

&lt;p&gt;Both of these things are a shutdown routine to help clear my mind so I don’t keep thinking about work for the rest of the day. If a work thought does pop into my head during the evening, I’ll quickly write that down on my todo list so I don’t have to worry about trying to remember it tomorrow. This helps reduce the cognitive load, lets me focus on my family, and also ensures that I don’t lose any “aha” moments when a sudden stroke of insight occurs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I’ve more or less followed this routine for years now, and it’s helped me immensely. I hope something in this piece has resonated with you and will help you too! Thanks for reading.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>programming</category>
      <category>career</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Heroku for ChatOps: Start and Monitor Deployments from Slack</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Tue, 04 Jun 2024 14:22:23 +0000</pubDate>
      <link>https://forem.com/thawkin3/heroku-for-chatops-start-and-monitor-deployments-from-slack-3kle</link>
      <guid>https://forem.com/thawkin3/heroku-for-chatops-start-and-monitor-deployments-from-slack-3kle</guid>
      <description>&lt;p&gt;In our last two articles, we explored how to &lt;a href="https://dev.to/thawkin3/how-i-finally-got-all-my-cicd-in-one-place-getting-my-cicd-act-together-with-heroku-flow-4fo2"&gt;configure CI/CD for Heroku using Heroku pipelines&lt;/a&gt;. When viewing a pipeline within the Heroku dashboard, you can easily &lt;a href="https://dev.to/thawkin3/going-with-the-flow-for-cicd-heroku-flow-with-gitflow-28oj"&gt;start a deployment or promote your code from one environment to the next&lt;/a&gt; with the click of a button. From the dashboard, you can monitor the deployment and view its progress.&lt;/p&gt;

&lt;p&gt;This all works really well, assuming that you have Heroku open in your browser. But, what if you wanted to do it all from Slack?&lt;/p&gt;

&lt;p&gt;Software engineers use a lot of apps at work. Throughout the day, we are constantly bouncing between Zoom meetings, Jira tasks, Slack conversations, GitHub, email, our calendar, and our IDE. This context switching can be exhausting and also lead to a lot of visual clutter on our monitors.&lt;/p&gt;

&lt;p&gt;Sometimes, it’s nice to just live in Slack, and that’s why many tools offer Slack integrations. With these Slack integrations, you can monitor various processes and even use shortcut commands to trigger actions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devcenter.heroku.com/articles/chatops" rel="noopener noreferrer"&gt;Heroku ChatOps&lt;/a&gt;, the Heroku Slack integration, allows you to start and monitor deployments directly from Slack. In this article, we’ll explore some of the Slack commands it offers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you’d like to follow along throughout this tutorial, you’ll need a Heroku account and a GitHub account. You can &lt;a href="https://www.heroku.com/" rel="noopener noreferrer"&gt;create a Heroku account here&lt;/a&gt;, and you can &lt;a href="https://github.com" rel="noopener noreferrer"&gt;create a GitHub account here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The demo app that we will use with our Heroku pipeline in this article is deployed to Heroku, and &lt;a href="https://github.com/thawkin3/heroku-flow-demo" rel="noopener noreferrer"&gt;the code is hosted on GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Create Our Heroku Pipeline
&lt;/h2&gt;

&lt;p&gt;We won’t go through the step-by-step process for creating a Heroku pipeline in this article. Refer to these articles for a walkthrough of creating a Heroku pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/thawkin3/how-i-finally-got-all-my-cicd-in-one-place-getting-my-cicd-act-together-with-heroku-flow-4fo2"&gt;How to create a Heroku pipeline with a staging and production app and a single main branch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/thawkin3/going-with-the-flow-for-cicd-heroku-flow-with-gitflow-28oj"&gt;How to create a Heroku pipeline with a staging and production app with a dev branch and a main branch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also read the &lt;a href="https://devcenter.heroku.com/articles/pipelines" rel="noopener noreferrer"&gt;Heroku docs for Heroku pipelines&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Configuring your Heroku pipeline includes the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a GitHub repo.&lt;/li&gt;
&lt;li&gt;Create a Heroku pipeline.&lt;/li&gt;
&lt;li&gt;Connect the GitHub repo to the Heroku pipeline.&lt;/li&gt;
&lt;li&gt;Add a staging app to the pipeline.&lt;/li&gt;
&lt;li&gt;Add a production app to the pipeline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The other activities that you’ll see in those articles, such as configuring review apps, Heroku CI, or automated deployments are optional. In fact, for the purposes of this demo, I recommend &lt;em&gt;not&lt;/em&gt; configuring automated deployments, since we’ll be using some Slack commands to start the deployments.&lt;/p&gt;

&lt;p&gt;When you’re done, you should have a Heroku pipeline that looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc8fgk3zahhhhxez1ov86.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc8fgk3zahhhhxez1ov86.png" alt="Example Heroku pipeline" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;
Example Heroku pipeline






&lt;h2&gt;
  
  
  Connect to Slack
&lt;/h2&gt;

&lt;p&gt;Now that you have your Heroku pipeline created, it’s time for the fun part: integrating with Slack. You can &lt;a href="https://chatops.heroku.com/auth/slack_install" rel="noopener noreferrer"&gt;install the Heroku ChatOps Slack app here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Clicking that link will prompt you to grant the Heroku ChatOps app permission to access your Slack workspace:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcexipm1efj6euq5fyzjz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcexipm1efj6euq5fyzjz.png" alt="Grant Heroku ChatOps access to your Slack workspace" width="800" height="872"&gt;&lt;/a&gt;&lt;/p&gt;
Grant Heroku ChatOps access to your Slack workspace



&lt;p&gt;After that, you can add the Heroku ChatOps app to any Slack channel in your workspace.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxklgk20kto6p552abkup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxklgk20kto6p552abkup.png" alt="Add the Heroku ChatOps app" width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;
Add the Heroku ChatOps app



&lt;p&gt;After adding the app, type &lt;code&gt;/h login&lt;/code&gt; and hit Enter. This will prompt you to connect your Heroku and GitHub accounts. You’ll see several Heroku OAuth and GitHub OAuth screens where you confirm connecting these accounts.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(As a personal anecdote, I found that it took me several tries to connect my Heroku account and my GitHub account. It may be due to having several Slack workspaces to choose from, but I’m not sure.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After connecting your Heroku account and your GitHub account, you’re ready to start using Heroku in Slack.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpmx16klatfjfcditeseh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpmx16klatfjfcditeseh.png" alt="Connect your Heroku and GitHub accounts" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;
Connect your Heroku and GitHub accounts






&lt;h2&gt;
  
  
  View All Pipelines
&lt;/h2&gt;

&lt;p&gt;To view all deployable pipelines, you can type &lt;code&gt;/h pipelines&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzw6n8w7uvk3sclvrhes2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzw6n8w7uvk3sclvrhes2.png" alt="View all pipelines" width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;
View all pipelines






&lt;h2&gt;
  
  
  View Pipeline Info
&lt;/h2&gt;

&lt;p&gt;To see information about any given pipeline, type &lt;code&gt;/h info &amp;lt;PIPELINE_NAME&amp;gt;&lt;/code&gt;. (Anything you see in angle brackets throughout this article should be replaced by an actual value. In this case, the value would be the name of a pipeline — for example, “heroku-flow-demo-pipeline”.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu0vc35lqby9nbplrt2c1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu0vc35lqby9nbplrt2c1.png" alt="View pipeline info" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;
View pipeline info






&lt;h2&gt;
  
  
  View Past Releases
&lt;/h2&gt;

&lt;p&gt;To view a history of past releases for any given pipeline, type &lt;code&gt;/h releases &amp;lt;PIPELINE_NAME&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwxighq4krp4y277yl9kc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwxighq4krp4y277yl9kc.png" alt="View past releases" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;
View past releases



&lt;p&gt;This command defaults to showing you past releases for the production app, so if you want to see the past releases for the staging app, you can type &lt;code&gt;/h releases &amp;lt;PIPELINE_NAME&amp;gt; in &amp;lt;STAGE_NAME&amp;gt;&lt;/code&gt;, where &lt;code&gt;&amp;lt;STAGE_NAME&amp;gt;&lt;/code&gt; is “staging”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fawt6gtlwm3ad5db6ed3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fawt6gtlwm3ad5db6ed3j.png" alt="View past staging releases" width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;
View past staging releases






&lt;h2&gt;
  
  
  Deploy to Staging
&lt;/h2&gt;

&lt;p&gt;Now that we know which pipelines are available, we can see information about any given pipeline along with when the code was last released for that pipeline. We’re ready to trigger a deployment.&lt;/p&gt;

&lt;p&gt;Most engineering organizations have a Slack channel (or channels) where they monitor deployments. Imagine being able to start a deployment right from that channel and monitor it as it goes out! That’s exactly what we’ll do next.&lt;/p&gt;

&lt;p&gt;To start a deployment to your staging environment, type &lt;code&gt;/h deploy &amp;lt;PIPELINE_NAME&amp;gt; to &amp;lt;STAGE_NAME&amp;gt;&lt;/code&gt;, where &lt;code&gt;&amp;lt;STAGE_NAME&amp;gt;&lt;/code&gt; is “staging”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgwakcqssera1qazvx1e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgwakcqssera1qazvx1e.png" alt="Deploy to staging" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;
Deploy to staging



&lt;p&gt;After running that command, an initial message is posted to communicate that the app is being deployed. Shortly after, you’ll also see several more messages, this time in a Slack thread on the original message:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F465uofqn6qk132udbd17.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F465uofqn6qk132udbd17.png" alt="Slack messages sent when deploying to staging" width="800" height="676"&gt;&lt;/a&gt;&lt;/p&gt;
Slack messages sent when deploying to staging



&lt;p&gt;If you want to verify what you’re seeing in Slack, you can always check the Heroku pipeline in your Heroku dashboard. You’ll see the same information: The staging app has been deployed!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5itkjco5vg1qb7gzvg3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5itkjco5vg1qb7gzvg3.png" alt="Staging app shown in the Heroku dashboard" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;
Staging app shown in the Heroku dashboard






&lt;h2&gt;
  
  
  Promote to Production
&lt;/h2&gt;

&lt;p&gt;Now, let’s promote our app to production. Without the Slack commands, we &lt;em&gt;could&lt;/em&gt; navigate to our Heroku pipeline, click the “Promote to production” button, and then confirm that action in the modal dialog that appears. However, we’d prefer to stay in Slack.&lt;/p&gt;

&lt;p&gt;To promote the app to production from Slack, type &lt;code&gt;/h promote &amp;lt;PIPELINE_NAME&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdiva9099ntl6dbhxw82r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdiva9099ntl6dbhxw82r.png" alt="Promote to production" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;
Promote to production



&lt;p&gt;Just like with the staging deployment, an initial Slack message will be sent, followed by several other messages as the production deployment goes out:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu26w1m51bzl5b8xcr7vu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu26w1m51bzl5b8xcr7vu.png" alt="Slack messages sent when promoting to production" width="800" height="519"&gt;&lt;/a&gt;&lt;/p&gt;
Slack messages sent when promoting to production



&lt;p&gt;And — voilà — the latest changes to the app are now in production!&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Now you can start and monitor Heroku app deployments all from Slack — no need to context switch or move between multiple apps.&lt;/p&gt;

&lt;p&gt;For more use cases and advanced setups, you can also &lt;a href="https://devcenter.heroku.com/articles/chatops" rel="noopener noreferrer"&gt;check out the docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy deploying!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>devops</category>
      <category>heroku</category>
    </item>
    <item>
      <title>Going with the Flow for CI/CD: Heroku Flow with Gitflow</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Wed, 08 May 2024 13:15:18 +0000</pubDate>
      <link>https://forem.com/thawkin3/going-with-the-flow-for-cicd-heroku-flow-with-gitflow-28oj</link>
      <guid>https://forem.com/thawkin3/going-with-the-flow-for-cicd-heroku-flow-with-gitflow-28oj</guid>
      <description>&lt;p&gt;In the previous article in this series, we looked at how we could automate deploys to Heroku &lt;a href="https://dev.to/thawkin3/how-i-finally-got-all-my-cicd-in-one-place-getting-my-cicd-act-together-with-heroku-flow-4fo2"&gt;using Heroku Flow for our CI/CD&lt;/a&gt;. That setup had two Heroku apps for staging and production, but only used a single &lt;code&gt;main&lt;/code&gt; branch. The staging app was automatically deployed when you committed to the &lt;code&gt;main&lt;/code&gt; branch, and the production app was manually deployed by clicking the “Promote to production” button in the Heroku pipeline.&lt;/p&gt;

&lt;p&gt;That strategy assumes that you’re doing “&lt;a href="https://www.atlassian.com/continuous-delivery/continuous-integration/trunk-based-development" rel="noopener noreferrer"&gt;trunk-based development&lt;/a&gt;”, in which you commit to a single &lt;code&gt;main&lt;/code&gt; branch in your code repo.&lt;/p&gt;

&lt;p&gt;However, some engineering teams still follow a branching strategy called &lt;a href="https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow" rel="noopener noreferrer"&gt;Gitflow&lt;/a&gt;, which involves using a &lt;code&gt;dev&lt;/code&gt; branch and a &lt;code&gt;main&lt;/code&gt; branch. Feature branches are merged into the &lt;code&gt;dev&lt;/code&gt; branch to deploy code to a staging environment, and on a regular cadence the &lt;code&gt;dev&lt;/code&gt; branch is merged into the &lt;code&gt;main&lt;/code&gt; branch to release the code to the production environment.&lt;/p&gt;

&lt;p&gt;In this article, we’ll consider how we can configure &lt;a href="https://www.heroku.com/flow" rel="noopener noreferrer"&gt;Heroku Flow&lt;/a&gt; to meet our CI/CD needs when following the Gitflow branching strategy. We’ll use our Heroku Pipeline to deploy our app to our staging environment when we push to the &lt;code&gt;dev&lt;/code&gt; branch and deploy our app to our production environment when we push to the &lt;code&gt;main&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;Ready to go with the flow?&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you’d like to follow along throughout this tutorial, you’ll need a Heroku account and a GitHub account. You can &lt;a href="https://www.heroku.com/" rel="noopener noreferrer"&gt;create a Heroku account here&lt;/a&gt;, and you can &lt;a href="https://github.com" rel="noopener noreferrer"&gt;create a GitHub account here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The demo app shown in this article is deployed to Heroku, and &lt;a href="https://github.com/thawkin3/heroku-flow-gitflow-demo" rel="noopener noreferrer"&gt;the code is hosted on GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running Our App Locally
&lt;/h2&gt;

&lt;p&gt;You can run the app locally by forking the repo in GitHub, installing dependencies, and running the start command. In your terminal, do the following after forking the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;heroku-flow-gitflow-demo
&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After starting the app, visit &lt;a href="http://localhost:5001/" rel="noopener noreferrer"&gt;http://localhost:5001/&lt;/a&gt; in your browser, and you’ll see the app running locally:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuk1vf6px2gibjxve97w0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuk1vf6px2gibjxve97w0.png" alt="Demo app" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;
Demo app






&lt;h2&gt;
  
  
  Creating Our Heroku Pipeline
&lt;/h2&gt;

&lt;p&gt;Now that we have the app running locally, let’s get it deployed to Heroku so that it can be accessed anywhere, not just on your machine.&lt;/p&gt;

&lt;p&gt;We’re going to create a Heroku pipeline that includes two apps: a staging app and a production app.&lt;/p&gt;

&lt;p&gt;To create a new Heroku pipeline, navigate to your Heroku dashboard, click the “New” button in the top-right corner of the screen, and then choose “Create new pipeline” from the menu.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0v6104ngpe2hok8w4jp8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0v6104ngpe2hok8w4jp8.png" alt="Create new pipeline" width="572" height="372"&gt;&lt;/a&gt;&lt;/p&gt;
Create new pipeline



&lt;p&gt;In the dialog that appears, give your pipeline a name, choose an owner (yourself), and connect your GitHub repo. If this is your first time connecting your GitHub account to Heroku, a second popup will appear in which you can confirm giving Heroku access to GitHub.&lt;/p&gt;

&lt;p&gt;After connecting to GitHub, click “Create pipeline” to finish the process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8rh7qql94rnubtlxsmk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8rh7qql94rnubtlxsmk.png" alt="Configure your pipeline" width="800" height="773"&gt;&lt;/a&gt;&lt;/p&gt;
Configure your pipeline



&lt;p&gt;With that, you’ve created a Heroku pipeline. Nice work!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzu5wnzox57zm7gaxyp4v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzu5wnzox57zm7gaxyp4v.png" alt="Newly created pipeline" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;
Newly created pipeline






&lt;h2&gt;
  
  
  Creating Our Staging and Production Apps
&lt;/h2&gt;

&lt;p&gt;As we mentioned, we’ll use two environments for our app: a staging environment and a production environment. The staging environment is where code is deployed for acceptance testing and any additional QA. The production environment is where code is released to actual users.&lt;/p&gt;

&lt;p&gt;Let’s add a staging app and a production app to our pipeline. Both of these apps will be based on the same GitHub repo.&lt;/p&gt;

&lt;p&gt;To add a staging app, click the “Add app” button in the Staging section. Next, click “Create new app” to open a side panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2copa4tqf7oddt63giq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2copa4tqf7oddt63giq.png" alt="Create a new staging app" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;
Create a new staging app



&lt;p&gt;In the side panel, give your app a name, choose an owner (yourself), and choose a region (I left mine in the United States). Then click “Create app” to confirm your changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqrrg35sayiqs55i2dpf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqrrg35sayiqs55i2dpf.png" alt="Configure your staging app" width="800" height="1033"&gt;&lt;/a&gt;&lt;/p&gt;
Configure your staging app



&lt;p&gt;Congrats, you’ve just created a staging app!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fahc6fahd49cvpj7r1d7c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fahc6fahd49cvpj7r1d7c.png" alt="Newly created staging app" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;
Newly created staging app



&lt;p&gt;Now let’s do the same thing, but this time for our production app. When you’re done configuring your production app, you should see both apps in your pipeline:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz37bpg9jgp2rwemd1o7r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz37bpg9jgp2rwemd1o7r.png" alt="Heroku pipeline with a staging app and a production app" width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;
Heroku pipeline with a staging app and a production app






&lt;h2&gt;
  
  
  Configuring Automatic Deploys
&lt;/h2&gt;

&lt;p&gt;We want our app to be deployed to our staging environment any time we commit to our repo’s &lt;code&gt;dev&lt;/code&gt; branch. To do this, click the dropdown button for the staging app and choose “Configure automatic deploys” from the menu.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Favzjj0uifdixlmvl2jm8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Favzjj0uifdixlmvl2jm8.png" alt="Configure automatic deploys for the staging app" width="800" height="607"&gt;&lt;/a&gt;&lt;/p&gt;
Configure automatic deploys for the staging app



&lt;p&gt;In the dialog that appears, make sure the &lt;code&gt;dev&lt;/code&gt; branch is targeted, and check the box to “Wait for CI to pass before deploy.” In a later step, we’ll configure Heroku CI so that we can run tests in a CI pipeline. We don’t want to deploy our app to our staging environment unless CI is passing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1jkwvfy6hbuxu1muyz7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1jkwvfy6hbuxu1muyz7.png" alt="Deploy the dev branch to the staging app after CI passes" width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;
Deploy the dev branch to the staging app after CI passes



&lt;p&gt;Now let’s do the same thing for the production app, but this time choosing to deploy our production app whenever we commit code to the &lt;code&gt;main&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhvdu73jkj7mqnf6bye5s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhvdu73jkj7mqnf6bye5s.png" alt="Configure automatic deploys for the production app" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;
Configure automatic deploys for the production app



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjvnkv3x525wkddpm7jpw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjvnkv3x525wkddpm7jpw.png" alt="Deploy the main branch to the production app after CI passes" width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;
Deploy the main branch to the production app after CI passes






&lt;h2&gt;
  
  
  Enabling Heroku CI
&lt;/h2&gt;

&lt;p&gt;If we’re going to require CI to pass, we had better have something configured for CI! Navigate to the “Tests” tab and then click the “Enable Heroku CI” button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqqv1u1ugami9u8ty1pru.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqqv1u1ugami9u8ty1pru.png" alt="Enable Heroku CI" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;
Enable Heroku CI



&lt;p&gt;Our demo app is built with Node and runs unit tests with Jest. The tests are run through the &lt;code&gt;npm test&lt;/code&gt; script. Heroku CI allows you to configure more complicated CI setups using an &lt;code&gt;app.json&lt;/code&gt; file, but in our case because the test setup is fairly basic, Heroku CI can figure out which command to run without any additional configuration on our part. Pretty neat!&lt;/p&gt;




&lt;h2&gt;
  
  
  Enabling Review Apps
&lt;/h2&gt;

&lt;p&gt;For the last part of our pipeline setup, let’s enable review apps. Review apps are temporary apps which get deployed for every pull request (PR) created in GitHub. They’re incredibly helpful when you want your code reviewer to manually review your changes. With a review app in place, the reviewer can simply open the review app rather than having to pull down the code onto their machine to run the app locally.&lt;/p&gt;

&lt;p&gt;To enable review apps, click the “Enable Review Apps” button on the pipeline page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvv57gtcbbb3a7i0hmh3f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvv57gtcbbb3a7i0hmh3f.png" alt="Enable Review Apps" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;
Enable Review Apps



&lt;p&gt;In the dialog that appears, check all three boxes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first box enables review apps to be automatically created for each PR.&lt;/li&gt;
&lt;li&gt;The second box ensures that the review app isn’t created until CI passes.&lt;/li&gt;
&lt;li&gt;The third box sets a time limit on how long a stale review app should exist until it is destroyed. Review apps use Heroku resources just like your regular apps, so you don’t want these temporary apps sitting around unused and costing you or your company more money.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you’re done with your configuration, click “Enable Review Apps” to finalize your changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i58ygk8m3ay1eb6xjbw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i58ygk8m3ay1eb6xjbw.png" alt="Configure your review apps" width="800" height="1681"&gt;&lt;/a&gt;&lt;/p&gt;
Configure your review apps






&lt;h2&gt;
  
  
  Seeing It All in Action
&lt;/h2&gt;

&lt;p&gt;Alright, you made it! Let’s review what we’ve done so far.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We created a Heroku pipeline.&lt;/li&gt;
&lt;li&gt;We created a staging app and a production app for that pipeline.&lt;/li&gt;
&lt;li&gt;We enabled automatic deploys from the &lt;code&gt;dev&lt;/code&gt; branch to our staging app.&lt;/li&gt;
&lt;li&gt;We enabled automatic deploys from the &lt;code&gt;main&lt;/code&gt; branch to our production app.&lt;/li&gt;
&lt;li&gt;We enabled Heroku CI to run tests for every PR.&lt;/li&gt;
&lt;li&gt;We enabled the creation of Heroku review apps for every PR.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now let’s see it all in action.&lt;/p&gt;

&lt;p&gt;Create a feature branch off of your &lt;code&gt;dev&lt;/code&gt; branch with any code change you’d like, and then use that to create a PR in GitHub to merge that feature branch into dev. I made a very minor UI change, updating the heading text from “Heroku Flow Gitflow Demo” to “Heroku Flow Gitflow Branching”.&lt;/p&gt;

&lt;p&gt;Right after the PR is created, you’ll note that a new “check” gets created in GitHub for the Heroku CI pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsc58670jzgcwuxul0wh0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsc58670jzgcwuxul0wh0.png" alt="GitHub PR check for the Heroku CI pipeline" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;
GitHub PR check for the Heroku CI pipeline



&lt;p&gt;You can view the test output back in Heroku on your Tests tab:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2kfdvevycfgqeseiuyls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2kfdvevycfgqeseiuyls.png" alt="CI pipeline test output" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;
CI pipeline test output



&lt;p&gt;After the CI pipeline passes, you’ll note that another piece of info gets appended to your PR in GitHub. The review app is deployed, and GitHub shows a link to the review app. Click the “View deployment” button, and you’ll see a temporary Heroku app with your code changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz07yiuwjgxh560q05pi8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz07yiuwjgxh560q05pi8.png" alt="View deployment to see the review app" width="800" height="128"&gt;&lt;/a&gt;&lt;/p&gt;
View deployment to see the review app



&lt;p&gt;You can also find a link to the review app in your Heroku pipeline:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff9up77ah0mvt336o1cjj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff9up77ah0mvt336o1cjj.png" alt="Review app found in the Heroku pipeline" width="800" height="605"&gt;&lt;/a&gt;&lt;/p&gt;
Review app found in the Heroku pipeline



&lt;p&gt;Let’s assume that you’ve gotten a code review and that everything looks good. It’s time to merge your PR.&lt;/p&gt;

&lt;p&gt;After you’ve merged your PR, look back at the Heroku pipeline. You’ll see that Heroku automatically deployed the staging app since new code was committed to the &lt;code&gt;dev&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqtczw63y58qkf6msnfw0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqtczw63y58qkf6msnfw0.png" alt="Staging app was automatically deployed" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;
Staging app was automatically deployed



&lt;p&gt;At this point in the software development lifecycle, there might be some final QA or acceptance testing of the app in the staging environment. Let’s assume that everything still looks good and that you’re ready to release this change to your users in production.&lt;/p&gt;

&lt;p&gt;You can create another PR to merge the &lt;code&gt;dev&lt;/code&gt; branch into the &lt;code&gt;main&lt;/code&gt; branch, or you can do this from your terminal by checking out the &lt;code&gt;main&lt;/code&gt; branch and then running git merge &lt;code&gt;dev&lt;/code&gt; and git push.&lt;/p&gt;

&lt;p&gt;Once you’ve committed these changes to the &lt;code&gt;main&lt;/code&gt; branch, look at your Heroku pipeline again, and you’ll see that the production app was automatically deployed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jolosrt0gwyk1w03ylj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jolosrt0gwyk1w03ylj.png" alt="Production app was automatically deployed" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;
Production app was automatically deployed



&lt;p&gt;And with that, your changes are now in production for all your users to see. Way to go!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21oljo1fn68labrzpyit.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21oljo1fn68labrzpyit.png" alt="Updated demo app with new changes in production" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;
Updated demo app with new changes in production






&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If you’ve followed along this far, give yourself a pat on the back. Walking through these steps, we’ve configured everything we need for an enterprise-ready CI/CD solution for a team using the Gitflow branching strategy and multiple environments.&lt;/p&gt;

&lt;p&gt;It doesn’t matter if you prefer trunk-based development or Gitflow as your branching strategy — Heroku Flow can support either.&lt;/p&gt;

&lt;p&gt;Now not only can you host your apps on Heroku, but you can set up all of your CI/CD infrastructure on Heroku’s platform as well.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>heroku</category>
      <category>cicd</category>
    </item>
    <item>
      <title>How I Finally Got All My CI/CD in One Place: Getting my CI/CD act together with Heroku Flow</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Mon, 29 Apr 2024 13:05:32 +0000</pubDate>
      <link>https://forem.com/thawkin3/how-i-finally-got-all-my-cicd-in-one-place-getting-my-cicd-act-together-with-heroku-flow-4fo2</link>
      <guid>https://forem.com/thawkin3/how-i-finally-got-all-my-cicd-in-one-place-getting-my-cicd-act-together-with-heroku-flow-4fo2</guid>
      <description>&lt;p&gt;The Heroku team has long been an advocate of CI/CD. Their platform integrates with many third-party solutions like GitLab CI/CD or GitHub Actions.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/thawkin3/deploying-to-heroku-with-gitlab-cicd-54f6"&gt;a previous article&lt;/a&gt;, I demonstrated how you can configure your Heroku app with GitLab CI/CD to automatically deploy your app to production. In &lt;a href="https://dev.to/thawkin3/deploying-heroku-apps-to-staging-and-production-environments-with-gitlab-cicd-3m8h"&gt;a follow-up article&lt;/a&gt;, I walked you through a slightly more nuanced setup involving both a staging environment and a production environment.&lt;/p&gt;

&lt;p&gt;But if you want to go all in on Heroku, you can use a series of solutions called &lt;a href="https://www.heroku.com/flow" rel="noopener noreferrer"&gt;Heroku Flow&lt;/a&gt; to &lt;strong&gt;configure all your CI/CD&lt;/strong&gt; without any third parties. Heroku Flow brings together &lt;a href="https://devcenter.heroku.com/articles/pipelines" rel="noopener noreferrer"&gt;Heroku pipelines&lt;/a&gt;, &lt;a href="https://devcenter.heroku.com/articles/heroku-ci" rel="noopener noreferrer"&gt;Heroku CI&lt;/a&gt;, &lt;a href="https://devcenter.heroku.com/articles/github-integration-review-apps" rel="noopener noreferrer"&gt;Heroku review apps&lt;/a&gt;, a GitHub integration, and a release phase.&lt;/p&gt;

&lt;p&gt;In this article, I’ll show you how to set this up for your own projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Before we begin, if you’d like to follow along, you’ll need a Heroku account and a GitHub account. You can &lt;a href="https://signup.heroku.com/login" rel="noopener noreferrer"&gt;create a Heroku account here&lt;/a&gt;, and you can &lt;a href="https://github.com/signup" rel="noopener noreferrer"&gt;create a GitHub account here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The demo app shown in this article is deployed to Heroku, and &lt;a href="https://github.com/thawkin3/heroku-flow-demo" rel="noopener noreferrer"&gt;the code is hosted on GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running Our App Locally
&lt;/h2&gt;

&lt;p&gt;You can run the app locally by forking the repo in GitHub, installing dependencies, and running the start command. In your terminal, do the following after forking the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;heroku-flow-demo
&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After starting the app, visit &lt;a href="http://localhost:5001/" rel="noopener noreferrer"&gt;http://localhost:5001/&lt;/a&gt; in your browser, and you’ll see the app running locally:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jti4gisut7cygxkse1p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jti4gisut7cygxkse1p.png" alt="Demo app" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
Demo app






&lt;h2&gt;
  
  
  Creating Our Heroku Pipeline
&lt;/h2&gt;

&lt;p&gt;Now that we have the app running locally, let’s get it deployed to Heroku so that it can be accessed anywhere, not just on your machine.&lt;/p&gt;

&lt;p&gt;We’ll create a Heroku pipeline that includes a staging app and a production app.&lt;/p&gt;

&lt;p&gt;To create a new Heroku pipeline, navigate to your Heroku dashboard, click the “New” button in the top-right corner of the screen, and then choose “Create new pipeline” from the menu.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68dw05ah1564s1ffastg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68dw05ah1564s1ffastg.png" alt="Create new pipeline" width="572" height="372"&gt;&lt;/a&gt;&lt;/p&gt;
Create new pipeline



&lt;p&gt;In the dialog that appears, give your pipeline a name, choose an owner (yourself), and connect your GitHub repo. If this is your first time connecting your GitHub account to Heroku, a second popup will appear in which you can confirm giving Heroku access to GitHub.&lt;/p&gt;

&lt;p&gt;After connecting to GitHub, click “Create pipeline” to finish the process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwo8zx4qi5hi5c2sk391x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwo8zx4qi5hi5c2sk391x.png" alt="Configure your pipeline" width="800" height="783"&gt;&lt;/a&gt;&lt;/p&gt;
Configure your pipeline



&lt;p&gt;With that, you’ve created a Heroku pipeline. Well done!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb4a0w0ke7eaxuohr91r4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb4a0w0ke7eaxuohr91r4.png" alt="Newly created pipeline" width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;
Newly created pipeline






&lt;h2&gt;
  
  
  Creating Our Staging and Production Apps
&lt;/h2&gt;

&lt;p&gt;Most engineering organizations use at least two environments: a staging environment and a production environment. The staging environment is where code is deployed for acceptance testing and any additional QA. Code in the staging environment is then promoted to the production environment to be released to actual users.&lt;/p&gt;

&lt;p&gt;Let’s add a staging app and a production app to our pipeline. Both of these apps will be based on the same GitHub repo.&lt;/p&gt;

&lt;p&gt;To add a staging app, click the “Add app” button in the “Staging” section. Next, click “Create new app” to open a side panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61bv1qkwo3f4ifixmkxg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61bv1qkwo3f4ifixmkxg.png" alt="Create a new staging app" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;
Create a new staging app



&lt;p&gt;In the side panel, give your app a name, choose an owner (yourself), and choose a region (I left mine in the United States). Then click “Create app” to confirm your changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi546c3lhfvkwms4i2mh0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi546c3lhfvkwms4i2mh0.png" alt="Configure your staging app" width="800" height="1044"&gt;&lt;/a&gt;&lt;/p&gt;
Configure your staging app



&lt;p&gt;Congrats, you’ve just created a staging app!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fum7ga0wqp6xng40ajmam.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fum7ga0wqp6xng40ajmam.png" alt="Newly created staging app" width="790" height="310"&gt;&lt;/a&gt;&lt;/p&gt;
Newly created staging app



&lt;p&gt;Now let’s do the same thing, but this time for our production app. When you’re done configuring your production app, you should see both apps in your pipeline:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpc6q4izgj7tx1ydgwzc7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpc6q4izgj7tx1ydgwzc7.png" alt="Heroku pipeline with a staging app and a production app" width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;
Heroku pipeline with a staging app and a production app






&lt;h2&gt;
  
  
  Configuring Automatic Deploys
&lt;/h2&gt;

&lt;p&gt;We want our app to be deployed to our staging environment any time we commit to our repo’s &lt;code&gt;main&lt;/code&gt; branch. To do this, click the dropdown button for the staging app and choose “Configure automatic deploys” from the menu.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F29xwkgvt0nyxc27m45se.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F29xwkgvt0nyxc27m45se.png" alt="Configure automatic deploys" width="800" height="628"&gt;&lt;/a&gt;&lt;/p&gt;
Configure automatic deploys



&lt;p&gt;In the dialog that appears, make sure the &lt;code&gt;main&lt;/code&gt; branch is targeted, and check the box to “Wait for CI to pass before deploy.” In our next step, we’ll configure Heroku CI so that we can run tests in a CI pipeline. We don’t want to deploy our app to our staging environment unless CI is passing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvnk6ggefcc22v7iowhcx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvnk6ggefcc22v7iowhcx.png" alt="Deploy the main branch to the staging app after CI passes" width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;
Deploy the main branch to the staging app after CI passes






&lt;h2&gt;
  
  
  Enabling Heroku CI
&lt;/h2&gt;

&lt;p&gt;If we’re going to require CI to pass, we better have something configured for CI! Navigate to the “Tests” tab and then click the “Enable Heroku CI” button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcn99ejnyfabji65gq8v8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcn99ejnyfabji65gq8v8.png" alt="Enable Heroku CI" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;
Enable Heroku CI



&lt;p&gt;Our demo app is built with Node and runs unit tests with Jest. The tests are run through the &lt;code&gt;npm test&lt;/code&gt; script. Heroku CI allows you to configure more complicated CI setups using an &lt;code&gt;app.json&lt;/code&gt; file, but in our case, because the test setup is fairly basic, Heroku CI can figure out which command to run without any additional configuration on our part. Pretty neat!&lt;/p&gt;




&lt;h2&gt;
  
  
  Enabling Review Apps
&lt;/h2&gt;

&lt;p&gt;For the last part of our pipeline setup, let’s enable review apps. Review apps are temporary apps that get deployed for every pull request (PR) created in GitHub. They’re incredibly helpful when you want your code reviewer to review your changes manually. With a review app in place, the reviewer can simply open the review app rather than having to pull down the code onto their machine and run the app locally.&lt;/p&gt;

&lt;p&gt;To enable review apps, click the “Enable Review Apps” button on the pipeline page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1yq7aez7w8gu35eb51xj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1yq7aez7w8gu35eb51xj.png" alt="Enable Review Apps" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
Enable Review Apps



&lt;p&gt;In the dialog that appears, check all three boxes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first box enables the automatic creation of review apps for each PR.&lt;/li&gt;
&lt;li&gt;The second box ensures that CI must pass before the review app can be created.&lt;/li&gt;
&lt;li&gt;The third box sets a time limit on how long a stale review app should exist until it is destroyed. Review apps use Heroku resources just like your regular apps, so you don’t want these temporary apps sitting around unused and costing you or your company more money.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you’re done with your configuration, click “Enable Review Apps” to finalize your changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2o3hctpma8zy03majds.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2o3hctpma8zy03majds.png" alt="Configure your review apps" width="800" height="1642"&gt;&lt;/a&gt;&lt;/p&gt;
Configure your review apps






&lt;h2&gt;
  
  
  Seeing It All in Action
&lt;/h2&gt;

&lt;p&gt;Alright, you made it! Let’s review what we’ve done so far.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We created a Heroku pipeline&lt;/li&gt;
&lt;li&gt;We created a staging app and a production app for that pipeline&lt;/li&gt;
&lt;li&gt;We enabled automatic deploys for our staging app&lt;/li&gt;
&lt;li&gt;We enabled Heroku CI to run tests for every PR&lt;/li&gt;
&lt;li&gt;We enabled Heroku review apps to be created for every PR&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now let’s see it all in action.&lt;/p&gt;

&lt;p&gt;Create a PR in GitHub with any code change you’d like. I made a very minor UI change, updating the heading text from “Heroku Flow Demo” to “Heroku Flow Rules!”&lt;/p&gt;

&lt;p&gt;Right after the PR is created, you’ll note that a new “check” gets created in GitHub for the Heroku CI pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5zna4gfuod8sbsgkktdc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5zna4gfuod8sbsgkktdc.png" alt="GitHub PR check for the Heroku CI pipeline" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;
GitHub PR check for the Heroku CI pipeline



&lt;p&gt;You can view the test output back in Heroku on your “Tests” tab:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cqks4skx5tgxfmodpm9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cqks4skx5tgxfmodpm9.png" alt="CI pipeline test output" width="800" height="545"&gt;&lt;/a&gt;&lt;/p&gt;
CI pipeline test output



&lt;p&gt;After the CI pipeline passes, you’ll note another piece of info gets appended to your PR in GitHub. The review app gets deployed, and GitHub shows a link to the review app. Click the “View deployment” button, and you’ll see a temporary Heroku app with your code changes in it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupor39jxy85s436wkgqk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupor39jxy85s436wkgqk.png" alt="View deployment to see the review app" width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;
View deployment to see the review app



&lt;p&gt;You can also find a link to the review app in your Heroku pipeline:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpq393qikh9slncbkei63.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpq393qikh9slncbkei63.png" alt="Review app found in the Heroku pipeline" width="752" height="638"&gt;&lt;/a&gt;&lt;/p&gt;
Review app found in the Heroku pipeline



&lt;p&gt;Let’s assume that you’ve gotten a code review and that everything looks good. It’s time to merge your PR.&lt;/p&gt;

&lt;p&gt;After you’ve merged your PR, look back at the Heroku pipeline. You’ll see that the staging app was automatically deployed since new code was committed to the &lt;code&gt;main&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmmkwwiqydh3hvoixfbb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmmkwwiqydh3hvoixfbb.png" alt="Staging app was automatically deployed" width="756" height="412"&gt;&lt;/a&gt;&lt;/p&gt;
Staging app was automatically deployed



&lt;p&gt;At this point in the software development lifecycle, there might be some final QA or acceptance testing of the app in the staging environment. Let’s assume that everything still looks good and that you’re ready to release this change to your users.&lt;/p&gt;

&lt;p&gt;Click the “Promote to production” button on the staging app. This will open a dialog for you to confirm your action. Click “Promote” to confirm your changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faa2w2t6g6eww4swr50c7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faa2w2t6g6eww4swr50c7.png" alt="Promote to production" width="800" height="748"&gt;&lt;/a&gt;&lt;/p&gt;
Promote to production



&lt;p&gt;After promoting the code, you’ll see the production app being deployed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgdpr5ckxojao7xmnada6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgdpr5ckxojao7xmnada6.png" alt="Production app was deployed" width="760" height="366"&gt;&lt;/a&gt;&lt;/p&gt;
Production app was deployed



&lt;p&gt;And with that, your changes are now in production for all of your users to enjoy. Nice work!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foe226cm3hwkxhzto9n25.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foe226cm3hwkxhzto9n25.png" alt="Updated demo app with new changes in production" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;
Updated demo app with new changes in production






&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;What a journey we’ve been through! In this short time together, we’ve configured everything we need for an enterprise-ready CI/CD solution.&lt;/p&gt;

&lt;p&gt;If you’d like to use a different CI/CD tool like GitLab CI/CD, GitHub Actions — or whatever else you may prefer — Heroku supports that as well.&lt;/p&gt;

&lt;p&gt;But if you don’t want to reach for a third-party CI/CD provider, now you can &lt;strong&gt;do it all&lt;/strong&gt; within Heroku with Heroku Flow.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>heroku</category>
      <category>programming</category>
    </item>
    <item>
      <title>Deploying Heroku Apps to Staging and Production Environments with GitLab CI/CD</title>
      <dc:creator>Tyler Hawkins</dc:creator>
      <pubDate>Mon, 25 Mar 2024 12:30:52 +0000</pubDate>
      <link>https://forem.com/thawkin3/deploying-heroku-apps-to-staging-and-production-environments-with-gitlab-cicd-3m8h</link>
      <guid>https://forem.com/thawkin3/deploying-heroku-apps-to-staging-and-production-environments-with-gitlab-cicd-3m8h</guid>
      <description>&lt;p&gt;In a &lt;a href="https://dev.to/thawkin3/deploying-to-heroku-with-gitlab-cicd-54f6"&gt;previous article&lt;/a&gt;, we explored how to automate deployments to Heroku using GitLab CI/CD. That setup deployed the app to its production environment every time we pushed code to the &lt;code&gt;main&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;In this article, we’ll consider a slightly more nuanced approach: What if we have multiple environments? Most engineering organizations use at least three environments: a local development environment, a staging environment, and a production environment.&lt;/p&gt;

&lt;p&gt;Additionally, some engineering teams follow a &lt;a href="https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow" rel="noopener noreferrer"&gt;Gitflow branching strategy&lt;/a&gt;, where they have a &lt;code&gt;dev&lt;/code&gt; branch and a &lt;code&gt;main&lt;/code&gt; branch. This strategy has since fallen out of favor and been replaced by &lt;a href="https://www.atlassian.com/continuous-delivery/continuous-integration/trunk-based-development" rel="noopener noreferrer"&gt;trunk-based development&lt;/a&gt;, but it’s not uncommon to find organizations still following this practice.&lt;/p&gt;

&lt;p&gt;Today, we will look at how to configure GitLab CI/CD to deploy our app to our staging environment when we push to the &lt;code&gt;dev&lt;/code&gt; branch and deploy our app to our production environment when we push to the &lt;code&gt;main&lt;/code&gt; branch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Before we begin, we’ll need two things: a Heroku account and a GitLab account.&lt;/p&gt;

&lt;p&gt;Heroku is a great place to host and deploy your apps. As a platform as a service (PaaS), Heroku allows you to focus on building cool things while abstracting away much of the infrastructure complexity. You can &lt;a href="https://signup.heroku.com/" rel="noopener noreferrer"&gt;create a Heroku account here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;GitLab is a great place to store your code. Beyond just being a source code management tool, GitLab also offers native CI/CD capabilities so you can set up pipelines to test and deploy your code without requiring another third-party tool. You can &lt;a href="https://gitlab.com/users/sign_up" rel="noopener noreferrer"&gt;create a GitLab account here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The demo app shown in this article uses both GitLab and Heroku. You can &lt;a href="https://gitlab.com/tylerhawkins1/heroku-gitflow-staging-production-gitlab-cicd-demo" rel="noopener noreferrer"&gt;find all the code in the GitLab repo here&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running Our App Locally
&lt;/h2&gt;

&lt;p&gt;You can run the app locally by cloning the repo, installing dependencies, and running the start command. In your terminal, do the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://gitlab.com/tylerhawkins1/heroku-gitflow-staging-production-gitlab-cicd-demo.git
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;heroku-gitflow-staging-production-gitlab-cicd-demo
&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After starting the app, visit &lt;a href="http://localhost:5001/" rel="noopener noreferrer"&gt;http://localhost:5001/&lt;/a&gt; in your browser, and you’ll see the app running locally:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl1rj1bfacnouhgzx1b8g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl1rj1bfacnouhgzx1b8g.png" alt="Demo Heroku app" width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;
Demo Heroku app






&lt;h2&gt;
  
  
  Deploying Our App to Heroku
&lt;/h2&gt;

&lt;p&gt;Now that the app is running locally, let’s deploy it to Heroku so that you can access it anywhere, not just on your machine.&lt;/p&gt;

&lt;p&gt;Remember that we are going to deploy your app to both a staging environment and a production environment. That means that we’ll have two Heroku apps based on the same GitLab repo.&lt;/p&gt;

&lt;p&gt;If you don’t already have the &lt;a href="https://devcenter.heroku.com/articles/heroku-cli" rel="noopener noreferrer"&gt;Heroku CLI&lt;/a&gt; installed on your machine, you’ll need to install that before moving on.&lt;/p&gt;

&lt;p&gt;After installing the Heroku CLI, run the following commands from your terminal to check out the &lt;code&gt;main&lt;/code&gt; branch, create a new production Heroku app, deploy it to your production environment, and open it in your browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git checkout main
&lt;span class="nv"&gt;$ &lt;/span&gt;heroku create heroku-gitlab-ci-cd-production &lt;span class="nt"&gt;--remote&lt;/span&gt; heroku-production
&lt;span class="nv"&gt;$ &lt;/span&gt;git push heroku-production main
&lt;span class="nv"&gt;$ &lt;/span&gt;heroku open &lt;span class="nt"&gt;--remote&lt;/span&gt; heroku-production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, you should see the same app, but this time running on a Heroku URL instead of on localhost. Nice work — you’ve deployed your Heroku app to production!&lt;/p&gt;

&lt;p&gt;But, we’re not done yet. We also need to configure and deploy your staging app. To do this, run this similar set of commands shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git checkout dev
&lt;span class="nv"&gt;$ &lt;/span&gt;heroku create heroku-gitlab-ci-cd-staging &lt;span class="nt"&gt;--remote&lt;/span&gt; heroku-staging
&lt;span class="nv"&gt;$ &lt;/span&gt;git push heroku-staging main
&lt;span class="nv"&gt;$ &lt;/span&gt;heroku open &lt;span class="nt"&gt;--remote&lt;/span&gt; heroku-staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you’ll have the same app deployed again, but this time to a different URL that will serve as your staging environment. Now we have both of your environments configured!&lt;/p&gt;

&lt;p&gt;Note the differences and similarities between the commands for the production app and the staging app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The production app uses the &lt;code&gt;main&lt;/code&gt; branch, and the staging app uses the &lt;code&gt;dev&lt;/code&gt; branch.&lt;/li&gt;
&lt;li&gt;The production app is called &lt;code&gt;heroku-gitlab-ci-cd-production&lt;/code&gt;, and the staging app is called &lt;code&gt;heroku-gitlab-ci-cd-staging&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The production app’s git remote is called &lt;code&gt;heroku-production&lt;/code&gt;, and the staging app’s git remote is called &lt;code&gt;heroku-staging&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Both the production app and the staging app’s git remotes use a &lt;code&gt;main&lt;/code&gt; branch, since &lt;a href="https://devcenter.heroku.com/articles/multiple-environments" rel="noopener noreferrer"&gt;Heroku only deploys the app when code is pushed to its main branch&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep in mind that &lt;em&gt;this&lt;/em&gt; &lt;code&gt;main&lt;/code&gt; branch is different from &lt;em&gt;your&lt;/em&gt; set of &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;dev&lt;/code&gt; branches. The &lt;code&gt;main&lt;/code&gt; branch that you’re pushing to here is the &lt;code&gt;main&lt;/code&gt; branch on your git remote — in this case, Heroku.&lt;/p&gt;

&lt;p&gt;So when you’re on your local &lt;code&gt;main&lt;/code&gt; branch and run &lt;code&gt;git push heroku-production main&lt;/code&gt;, you’re pushing your &lt;code&gt;main&lt;/code&gt; branch to the Heroku production app’s &lt;code&gt;main&lt;/code&gt; branch. And when you’re on your local &lt;code&gt;dev&lt;/code&gt; branch and run &lt;code&gt;git push heroku-staging main&lt;/code&gt;, you’re pushing your &lt;code&gt;dev&lt;/code&gt; branch to the Heroku staging app’s &lt;code&gt;main&lt;/code&gt; branch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Making Changes to Our App
&lt;/h2&gt;

&lt;p&gt;Now that we have our Heroku app up and running, what if we want to make some changes? Following a rough approximation of the Gitflow branching strategy, we could do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check out the &lt;code&gt;dev&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;Make changes to the code&lt;/li&gt;
&lt;li&gt;Add, commit, and push those changes to the &lt;code&gt;dev&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;Deploy those changes to the staging Heroku app by running &lt;code&gt;git push heroku-staging main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check out the &lt;code&gt;main&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;Merge the &lt;code&gt;dev&lt;/code&gt; branch into the &lt;code&gt;main&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;Deploy those changes to the production Heroku app by running &lt;code&gt;git push heroku-production main&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;(If we were truly following Gitflow, we’d have created a feature branch to merge into &lt;code&gt;dev&lt;/code&gt;, and a release branch to merge into &lt;code&gt;main&lt;/code&gt;, but we’ve omitted those steps to keep things simple.)&lt;/p&gt;

&lt;p&gt;Now, wouldn’t it be nice if we could automate the deployment to either of our environments instead of having to deploy them manually all the time?&lt;/p&gt;

&lt;p&gt;This is where GitLab CI/CD comes into play.&lt;/p&gt;




&lt;h2&gt;
  
  
  Continuous Integration / Continuous Deployment
&lt;/h2&gt;

&lt;p&gt;Continuous integration (CI) is all about committing often and keeping the build in a good state at all times. Typically you would verify that the build is in a good state by running checks in a CI pipeline. These checks might include linters, unit tests, and/or end-to-end tests.&lt;/p&gt;

&lt;p&gt;Continuous deployment (CD) is all about deploying frequently. If the checks in the CI pipeline pass, then the build gets deployed. If the checks in the CI pipeline fail, then the build does not get deployed.&lt;/p&gt;

&lt;p&gt;With GitLab CI/CD, we can configure our CI pipeline to do exactly this — run our tests and then deploy our app to Heroku if the tests all pass. Most importantly for our setup, we can configure rules within our CI pipeline to specify where the app should be deployed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuring GitLab CI/CD
&lt;/h2&gt;

&lt;p&gt;We can create a CI pipeline in GitLab programmatically using a &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file. Our file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:20.10.0&lt;/span&gt;

&lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/&lt;/span&gt;

&lt;span class="na"&gt;before_script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node -v&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm install&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;unit-test-job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;

&lt;span class="na"&gt;deploy-job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;HEROKU_APP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$HEROKU_APP_NAME_PRODUCTION&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH&lt;/span&gt;
      &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;HEROKU_APP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$HEROKU_APP_NAME_PRODUCTION&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_NAME =~ /dev/&lt;/span&gt;
      &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;HEROKU_APP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$HEROKU_APP_NAME_STAGING&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apt-get update -yq&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apt-get install -y ruby-dev&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gem install dpl&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitLab CI pipelines consist of stages and jobs. Each stage can contain one or more jobs. In our pipeline, we have two stages: &lt;code&gt;test&lt;/code&gt; and &lt;code&gt;deploy&lt;/code&gt;. In the &lt;code&gt;test&lt;/code&gt; stage, we run our unit tests in the &lt;code&gt;unit-test-job&lt;/code&gt;. In the &lt;code&gt;deploy&lt;/code&gt; stage, we deploy our app to Heroku in the &lt;code&gt;deploy-job&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can only progress to the next stage if all the jobs in the previous stage pass. That means if the unit tests fail, then the app won’t be deployed, which is a good thing! We wouldn’t want to deploy our app if it was in a bad state.&lt;/p&gt;

&lt;p&gt;You’ll note that the &lt;code&gt;deploy&lt;/code&gt; stage has a &lt;code&gt;rules&lt;/code&gt; section with some conditional logic in place. This is where we specify to which environment our app should be deployed. If we’re on the &lt;code&gt;main&lt;/code&gt; branch, then we deploy our production app. If we’re on the &lt;code&gt;dev&lt;/code&gt; branch, then we deploy our staging app.&lt;/p&gt;

&lt;p&gt;You’ll also note that the &lt;code&gt;deploy&lt;/code&gt; stage references several variables called &lt;code&gt;$HEROKU_APP_NAME_PRODUCTION&lt;/code&gt;, &lt;code&gt;$HEROKU_APP_NAME_STAGING&lt;/code&gt;, and &lt;code&gt;$HEROKU_API_KEY&lt;/code&gt;. These are stored as &lt;a href="https://docs.gitlab.com/ee/ci/variables/index.html" rel="noopener noreferrer"&gt;CI/CD variables within GitLab&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you’re setting this up in your own GitLab account, you’ll need to first find your API key in your Heroku account. Within your Heroku account settings, you should see a section for your API key. If you haven’t generated an API key yet, generate one now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuq4h8gcpcbmoy1cpx78f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuq4h8gcpcbmoy1cpx78f.png" alt="Heroku API key" width="800" height="135"&gt;&lt;/a&gt;&lt;/p&gt;
Heroku API key



&lt;p&gt;Next, in your GitLab project, click on &lt;strong&gt;Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;CI/CD&lt;/strong&gt; &amp;gt; &lt;strong&gt;Variables&lt;/strong&gt;. Expand that section and add three new variables:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The value for &lt;code&gt;$HEROKU_API_KEY&lt;/code&gt; will be the API key from your Heroku account.&lt;/li&gt;
&lt;li&gt;The value for &lt;code&gt;$HEROKU_APP_NAME_PRODUCTION&lt;/code&gt; will be the name of your production Heroku app. My production app’s name is &lt;code&gt;heroku-gitlab-ci-cd-production&lt;/code&gt;, but since Heroku app names are universally unique, yours will be something different.&lt;/li&gt;
&lt;li&gt;The value for &lt;code&gt;$HEROKU_APP_NAME_STAGING&lt;/code&gt; will be the name of your staging Heroku app. My staging app’s name is &lt;code&gt;heroku-gitlab-ci-cd-staging&lt;/code&gt;. Again, since Heroku app names are universally unique, yours will be something different.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0zz28s52bp184kzj4ge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0zz28s52bp184kzj4ge.png" alt="GitLab CI/CD variables" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;
GitLab CI/CD variables



&lt;p&gt;With that, your GitLab CI pipeline is ready to go! Let’s test it out.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploying Our Heroku App to the Staging Environment
&lt;/h2&gt;

&lt;p&gt;Let’s check out the &lt;code&gt;dev&lt;/code&gt; branch and make a change to the code in our app. I made a simple change to the heading text and added several new lines to the text in the UI. You can make a similar change in your code.&lt;/p&gt;

&lt;p&gt;Now add, commit, and push that change to the &lt;code&gt;dev&lt;/code&gt; branch. This will start the GitLab CI pipeline. You can view the pipeline within GitLab and see the progress in real time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F51h27b5i9kde731u2v16.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F51h27b5i9kde731u2v16.png" alt="GitLab CI pipeline build for the dev branch" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;
GitLab CI pipeline build for the dev branch



&lt;p&gt;If everything goes well, you should see that both the &lt;code&gt;test&lt;/code&gt; and &lt;code&gt;deploy&lt;/code&gt; stages have passed. Now, go check out your hosted Heroku app at the staging environment URL. The GitLab CI pipeline took care of deploying the app for you, so you’ll now see your changes live in the staging environment!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmskybuqll8u8r5jk2wsv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmskybuqll8u8r5jk2wsv.png" alt="Demo Heroku app with updated copy" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;
Demo Heroku app with updated copy



&lt;p&gt;With a staging environment set up, you now have a great place to manually test your code in a hosted environment. This is also a perfect spot for having QA testers or product managers verify changes before they go to production.&lt;/p&gt;

&lt;p&gt;Now, check out your production app URL. You’ll note that it’s still showing the old UI without the most recent changes. This is because the GitLab CI pipeline only deployed the changes to the staging environment, not the production environment.&lt;/p&gt;

&lt;p&gt;We’ve verified that the code looks good in our staging environment, so let’s promote it to our production environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploying Our Heroku App to the Production Environment
&lt;/h2&gt;

&lt;p&gt;Let’s check out the &lt;code&gt;main&lt;/code&gt; branch and then merge the &lt;code&gt;dev&lt;/code&gt; branch into your &lt;code&gt;main&lt;/code&gt; branch. You could do this through a pull request or by running &lt;code&gt;git merge dev&lt;/code&gt; and then &lt;code&gt;git push&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This will start the GitLab CI pipeline once again, but this time it will be preparing to deploy your production app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fslogny6r6cf26r96pk95.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fslogny6r6cf26r96pk95.png" alt="GitLab CI pipeline build for the main branch" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;
GitLab CI pipeline build for the main branch



&lt;p&gt;You can also view all the pipeline runs on the Pipelines page in GitLab to see the various builds for your &lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;main&lt;/code&gt; branches:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsr7bxwvvdvxj8wmbcuop.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsr7bxwvvdvxj8wmbcuop.png" alt="All GitLab CI pipeline builds" width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;
All GitLab CI pipeline builds



&lt;p&gt;Once the pipeline finishes, visit the URL for your production Heroku app. You should now see your changes also deployed in production. Great work!&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A good CI pipeline allows you to ship new features quickly and confidently without manually managing the deployment process. Having multiple environments configured for local development, staging, and production gives you more control over where and when code is released.&lt;/p&gt;

&lt;p&gt;Heroku and GitLab CI/CD allow you to automate all of this to make your DevOps processes a breeze!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cicd</category>
      <category>gitlab</category>
      <category>heroku</category>
    </item>
  </channel>
</rss>
