<?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>DEV Community: Mro</title>
    <description>The latest articles on DEV Community by Mro (@mro_automation).</description>
    <link>https://dev.to/mro_automation</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%2F1499204%2F4dfdeb1e-4910-4723-9f77-0baed66535bb.png</url>
      <title>DEV Community: Mro</title>
      <link>https://dev.to/mro_automation</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mro_automation"/>
    <language>en</language>
    <item>
      <title>Deep Dive: Google Apps Script - Testing APIs and Automating Sheets</title>
      <dc:creator>Mro</dc:creator>
      <pubDate>Wed, 15 May 2024 21:17:18 +0000</pubDate>
      <link>https://dev.to/mro_automation/deep-dive-google-apps-script-testing-apis-and-automating-sheets-khe</link>
      <guid>https://dev.to/mro_automation/deep-dive-google-apps-script-testing-apis-and-automating-sheets-khe</guid>
      <description>&lt;p&gt;In this article, we will explore how to test your Google Apps Script which uses external APIs to get data and then populate Google Sheets with it.&lt;/p&gt;

&lt;p&gt;We will use a trivial processing function which transforms the data we receive from the API. The point of this article is to focus on writing automated testing of the solution and not on the processing of the data, therefore we keep the processing as simple as possible.&lt;/p&gt;

&lt;p&gt;Let’s start by understanding the requirements:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Story:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As an HR manager&lt;br&gt;
I want to easily see all public holidays in the UK in my spreadsheet&lt;br&gt;
So that I use it for data analysis&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Scenario 1 - get data:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Given I have access to the Public Holidays API&lt;br&gt;
When I change the year and country in the dropdown&lt;br&gt;
Then I can see all related public holidays in the spreadsheet&lt;br&gt;
And I will see the Date, Name and Counties for each&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Scenario 2 - data processing:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When the public holidays are loaded in the spreadsheet&lt;br&gt;
Then the dates which are invalid are not displayed&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We will use this Public Holidays API endpoint:&lt;br&gt;
&lt;a href="https://date.nager.at/Api" rel="noopener noreferrer"&gt;https://date.nager.at/Api&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;We have already created a spreadsheet with the two input fields, year and country.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F95t2qjjel9s5j2w6y19n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F95t2qjjel9s5j2w6y19n.png" alt="Sheet"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will use the built-in lookup function to find the country code based on the list of countries in another sheet.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;=VLOOKUP(B2,Countries!A:B,2,FALSE)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Funuvddmdxa4fqp57qtqz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Funuvddmdxa4fqp57qtqz.png" alt="Lookup table"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have also added a Submit button and assigned a script to call our submit function.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fm4rhm6ue4ol66zzvthu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fm4rhm6ue4ol66zzvthu5.png" alt="Assign script"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, in order to make it easier to work with the input and output ranges, we have created three named ranges, two for the inputs called &lt;strong&gt;Year&lt;/strong&gt; and &lt;strong&gt;CountryCode&lt;/strong&gt; and one for the output called &lt;strong&gt;Holidays&lt;/strong&gt;. The &lt;strong&gt;Holidays&lt;/strong&gt; range starts on the second row and does not include the heading.&lt;/p&gt;

&lt;p&gt;This will make it easier to clear and replace the content.&lt;/p&gt;

&lt;p&gt;Now let’s have a look at the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial code and basic workflow
&lt;/h2&gt;

&lt;p&gt;First we need to get the input values from the sheets, the year and the country code.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function submit() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
  const year = sheet.getRangeByName('Year').getValue()
  const countryCode = sheet.getRangeByName('CountryCode').getValue()
  // …
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Let’s create a new function to do the processing, in order to separate the concerns of getting inputs and creating the output. We will call it &lt;code&gt;getAndProcessData&lt;/code&gt; and pass the two input variables. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function submit() {
  // …
  getAndProcessData(year, countryCode)
}

const getAndProcessData = (year, countryCode) =&amp;gt; {
  // …
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Note: For simplicity, I will omit the JavaScript documentation, but for a production grade code you may want to add the documentation (see &lt;a href="https://jsdoc.app" rel="noopener noreferrer"&gt;jsdoc.app&lt;/a&gt; website for more).&lt;/p&gt;

&lt;p&gt;Let’s elaborate on the workflow. We need to call an external API to fetch the data, if we get the data successfully then we also need to clear the range to ensure we don’t keep any previous data in it, and finally we need to process the data and populate the range.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const getAndProcessData = (year, countryCode) =&amp;gt; {
  const data = getData(year, countryCode)
  if(data) {
    // clear target range
    // process data
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Fetching data from API
&lt;/h2&gt;

&lt;p&gt;Getting the data is trivial, using the Url Fetch App and fetch function. In order to process the data as a JSON object we need to parse the text response. Handling issues with fetching and parsing the data is outside of the scope of this video, but feel free to comment and ask for another deep dive on how to handle and test that.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const getData = (year, countryCode) =&amp;gt; {
  // see https://date.nager.at/Api
  const url = `https://date.nager.at/api/v3/publicholidays/${year}/${countryCode}`
  const response = UrlFetchApp.fetch(url)

  const json = response.getContentText()
  const data = JSON.parse(json)
  return data
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now we have the data as a JSON object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the code with AAA
&lt;/h2&gt;

&lt;p&gt;Disclaimer:&lt;br&gt;
Not strictly following the &lt;a href="https://en.wikipedia.org/wiki/Test-driven_development" rel="noopener noreferrer"&gt;TDD (Test Driven Development)&lt;/a&gt; in this simple example, we are writing some skeleton code first and test it next and then complete the implementation to make the tests pass. This can illustrate how this process would work when adding tests to an existing code.&lt;/p&gt;

&lt;p&gt;It is a good idea to explore the API and get some sample data for testing.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; curl https://date.nager.at/api/v3/publicholidays/2024/GB | jq

[
  {
    "date": "2024-01-01",
    "localName": "New Year's Day",
    "name": "New Year's Day",
    "countryCode": "GB",
    "fixed": false,
    "global": false,
    "counties": [
      "GB-NIR"
    ],
    "launchYear": null,
    "types": [
      "Public"
    ]
  },
  {
    "date": "2024-01-01",
    "localName": "New Year's Day",
    "name": "New Year's Day",
    "countryCode": "GB",
    "fixed": false,
    "global": false,
    "counties": [
      "GB-ENG",
      "GB-WLS"
    ],
    "launchYear": null,
    "types": [
      "Public"
    ]
  },
…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Let’s create our first test. It will be failing at this point, but that’s OK, because we have not completed the implementation yet.&lt;/p&gt;

&lt;p&gt;Let’s follow the triple A pattern for unit testing - &lt;strong&gt;Arrange&lt;/strong&gt;, &lt;strong&gt;Act&lt;/strong&gt;, &lt;strong&gt;Assert&lt;/strong&gt;. This pattern is popular amongst .NET developers (see e.g. &lt;a href="https://www.c-sharpcorner.com/UploadFile/dacca2/fundamental-of-unit-testing-understand-aaa-in-unit-testing/" rel="noopener noreferrer"&gt;here&lt;/a&gt;).&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_getAndProcessData = () =&amp;gt; {
  Logger.log('test_getAndProcessData')

  // Arrange
  // Act
  // Assert
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;First we &lt;strong&gt;Arrange&lt;/strong&gt; or setup the test - we need a test year, test country code and mocked sample data.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_getAndProcessData = () =&amp;gt; {
  // …

  // Arrange
  const year = 2024
  const countryCode = 'UK'
  const sampleData = [
    {
      "date": new Date("2024-05-27"),
      "localName": "Spring Bank Holiday",
      "name": "Spring Bank Holiday",
      "countryCode": "GB",
      "fixed": false,
      "global": true,
      "counties": null,
      "launchYear": null,
      "types": [
        "Public"
      ]
    },
    {
      "date": new Date("2024-08-05"),
      "localName": "Summer Bank Holiday",
      "name": "Summer Bank Holiday",
      "countryCode": "GB",
      "fixed": false,
      "global": false,
      "counties": [
        "GB-SCT"
      ],
      "launchYear": null,
      "types": [
        "Public"
      ]
    }
  ]
  // …
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Reusable Functions
&lt;/h2&gt;

&lt;p&gt;We also need the target range, which we call &lt;code&gt;Holidays&lt;/code&gt;. That is a separate concern so we create a separate function just to get that range. Let’s call it &lt;code&gt;getDataRange&lt;/code&gt;. Let’s use the built-in &lt;code&gt;SpreadsheetApp.getActiveSpreadsheet()&lt;/code&gt; function. Then we use &lt;code&gt;getRangeByName(…)&lt;/code&gt;, and pass the argument, the name is "Holidays".&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_getAndProcessData = () =&amp;gt; {
  // …

  // Arrange
  // …
  const range = getDataRange()
}

const getDataRange = () =&amp;gt; {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
  return sheet.getRangeByName('Holidays')
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The test function will test the &lt;code&gt;getAndProcessData&lt;/code&gt; function, so that goes under the Act section.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_getAndProcessData = () =&amp;gt; {
  // …

  // Act
  getAndProcessData(year, countryCode)

  // …
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Mocking
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This is where the testing gets exciting!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We don’t want to be calling a real API in our unit tests! But if I would execute this test, it would call the real API. How to handle it?&lt;/p&gt;

&lt;p&gt;We are going to refactor the &lt;code&gt;getData&lt;/code&gt; function and instead of calling the &lt;code&gt;UrlFetchApp.fetch&lt;/code&gt; function, we pass in a fetch function as a parameter into the &lt;code&gt;getData&lt;/code&gt; function. During normal runs, this would be the &lt;code&gt;UrlFetchApp.fetch&lt;/code&gt;, and during test runs it would be our mocked fetch function, which will not call the actual external API and which we will have fully under control in the test code.&lt;/p&gt;

&lt;p&gt;Let’s go back to the test. Let’s create a mocked fetch function, which returns a response object, which then has a &lt;code&gt;getContentText&lt;/code&gt; property, which is a function, which then returns a JSON string, just like the &lt;code&gt;UrlFetchApp.fetch&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_getAndProcessData = () =&amp;gt; {
  // …

  // Arrange
  // …
  const mockedFetch = (url) =&amp;gt; {
    return {
      getContentText: () =&amp;gt; JSON.stringify(sampleData)
    }
  }

  // …
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Let’s change the call to pass this mocked fetch function as a parameter.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function submit() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
  const year = sheet.getRangeByName('Year').getValue()
  const countryCode = sheet.getRangeByName('CountryCode').getValue()
  getAndProcessData(year, countryCode, UrlFetchApp.fetch)
}

const getAndProcessData = (year, countryCode, fetchFn) =&amp;gt; {
  const data = getData(year, countryCode, fetchFn)
  if(data) {
    clearRange()
    processData(data)
  }
}

const getData = (year, countryCode, fetchFn) =&amp;gt; {
  // see https://date.nager.at/Api
  const url = `https://date.nager.at/api/v3/publicholidays/${year}/${countryCode}`
  const response = fetchFn(url)

  const json = response.getContentText()
  const data = JSON.parse(json)
  return data
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;… and in the test:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_getAndProcessData = () =&amp;gt; {
  // …

  // Act
  getAndProcessData(year, countryCode, mockedFetch)

  // …
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Assertion
&lt;/h2&gt;

&lt;p&gt;Now, we can assert the output, to ensure that the actual behavior meets the expected behavior.&lt;/p&gt;

&lt;p&gt;To make the assertions easier, add variables for the two expected rows we should see in the target range. These match the mocked input JSON with its two objects.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_getAndProcessData = () =&amp;gt; {
  // …

  // Arrange
  const expectedRow1 = [new Date("2024-05-27"), "Spring Bank Holiday", 'null']
  const expectedRow2 = [new Date("2024-08-05"), "Summer Bank Holiday", '["GB-SCT"]']

  // …
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We get the actual values from the range and create the expected data range which has the same size and set its rows to the expected values - we have just the two rows.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_getAndProcessData = () =&amp;gt; {
  // …

  // Assert
  const actual = range.getValues()
  const expected = [] // TODO: create array with the same shape as the actual array
  expected[0] = expectedRow1
  expected[1] = expectedRow2
  // TODO: assert that actual equals expected

  // …
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Multidimensional arrays
&lt;/h2&gt;

&lt;p&gt;Creating a multidimensional array of a certain size is again a different concern, so let’s create a new function for that. It will take just one parameter since we only need to create some number of rows but for this example we will hard-code the number of columns to three. Use the spread operator to generate an array of a desired size, and a map function to generate a new array where each item is another array with three empty strings.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const createArrayWithThreeColumns = (rows) =&amp;gt; {
  return [...Array(rows).keys()].map(() =&amp;gt; ['', '', ''])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This isn’t a trivial code, so let’s add a simple test for that. Just a single test will do for now, let’s say with two rows. Run the test… and it’s passing. Great!&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_createArrayWithThreeColumns = () =&amp;gt; {
  // Arrange
  const rows = 2
  const expected = [
    ['', '', ''],
    ['', '', ''],
  ]
  // Act
  const actual = createArrayWithThreeColumns(rows)
  // Assert
  assertEquals(actual, expected)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Main test
&lt;/h2&gt;

&lt;p&gt;Now back to the main test.&lt;/p&gt;

&lt;p&gt;We can compare the actual and expected values. Since these are multidimensional arrays, the easiest way to compare them is to convert them to strings and compare the strings. Comparing the values is a good candidate for another reusable function to keep clean separation of concerns, so let’s go ahead and create a new function for that. It will log a &lt;code&gt;passed&lt;/code&gt; message if the test fails and log an error if it fails. It’s useful to also log the expected and actual values. We are not going to write a test for that though.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_getAndProcessData = () =&amp;gt; {
  // …

  // Assert
  const actual = range.getValues()
  const expected = createArrayWithThreeColumns(range.getNumRows())
  expected[0] = expectedRow1
  expected[1] = expectedRow2
  // assert actual equals expected
  assertEquals(actual, expected)
}

const assertEquals = (actual, expected) =&amp;gt; {
  if(JSON.stringify(actual) === JSON.stringify(expected)) {
    Logger.log('- passed')
  } else {
    Logger.log(new Error(`- failed; expected: ${expected} actual: ${actual}`))
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Back to the main test. Run it and… it should fail. That is expected. Let’s move on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fp4rdgcm97nvchpacx41u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fp4rdgcm97nvchpacx41u.png" alt="Expected test failure - missing implementation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Clearing the table
&lt;/h2&gt;

&lt;p&gt;Let’s go back to the top. We need a way to clear any existing data in the target range which we named &lt;code&gt;Holidays&lt;/code&gt;. Get the active spreadsheet, get the range by name and call the clear function.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const getAndProcessData = (year, countryCode) =&amp;gt; {
  const data = getData(year, countryCode)
  if(data) {
    clearRange()
    // process data
  }
}

…

const clearRange = () =&amp;gt; {
  getDataRange().clear()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This is trivial, should we test it too?&lt;/p&gt;

&lt;p&gt;Yes. It is a part of the overall solution and there are already quite a few moving parts.&lt;/p&gt;

&lt;p&gt;Let’s create a new test function. Under the Arrange section, get the range and set the value to something like a &lt;code&gt;'TEST'&lt;/code&gt;. This is referencing the actual spreadsheet, so be aware that when this test is run, it will overwrite the data in the spreadsheet. But I am OK with that for now. Under the &lt;strong&gt;Act&lt;/strong&gt; section, call the &lt;code&gt;clearRange()&lt;/code&gt; function. Finally under the &lt;strong&gt;Assert&lt;/strong&gt; section, check that the range is empty again.&lt;/p&gt;

&lt;p&gt;The actual data is the range values and the expected data is an empty array; we will reuse the functions created above to create the array and to compare the actual and expected values.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_clearRange = () =&amp;gt; {
  Logger.log('test_clearRange')

  // Arrange
  const range = getDataRange()
  range.setValue('TEST')

  // Act
  clearRange()

  // Assert
  const actual = range.getValues()
  const expected = createArrayWithThreeColumns(range.getNumRows())
  assertEquals(actual, expected)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Does it work? Let’s put a breakpoint to see it in action. You can select this test function in the menu and click on the &lt;strong&gt;Debug&lt;/strong&gt; button. Then wait for a while and the debugger will pause on the breakpoint. Switch to the spreadsheet and you will see that the target range is populated with the TEST text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F2s0petk46dhawkveq5rn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F2s0petk46dhawkveq5rn.png" alt="Debugger Breakpoint"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F1kmaaqfzre2hyfl9hns1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F1kmaaqfzre2hyfl9hns1.png" alt="TEST TEST TEST"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Continue running the code. The test has passed! Switch back to the spreadsheet and you will see that the target range is now empty. Great!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fuychos0f51s6bgob2xe1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fuychos0f51s6bgob2xe1.png" alt="Empty range"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Processing
&lt;/h2&gt;

&lt;p&gt;The final step is processing of the data and populating the target range.&lt;/p&gt;

&lt;p&gt;Get the target range using the &lt;code&gt;getDataRange()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;In order to make it easier to populate the whole range at once, instead of row by row or cell by cell, create a multidimensional array that has the same size as the target range and populate it with the values we get from the API call. Reuse the function created earlier.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const processData = (data) =&amp;gt; {
  const target = getDataRange()
  const rows = createArrayWithThreeColumns(target.getNumRows())
  // …
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Iterate over the data and for each value and index pair, add the desired elements to the row on the relevant index.&lt;/p&gt;

&lt;p&gt;Here is where our data processing logic would be added - for simplicity again I have only a quick check that the date value is a valid date and if not it will be ignored. A production code would at least log a message as well for better observability but I’m OK with keeping this simple for now.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const processData = (data) =&amp;gt; {
  // …

  data.forEach((value, index) =&amp;gt; {
    // business logic - e.g. ignore invalid dates
    if(new Date(value.date) == 'Invalid Date') return

    // populate the output array
    rows[index] = [value.date, value.name, JSON.stringify(value.counties)]
  })
}
  // …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The last instruction is to set the values on the target range. Since we have a multidimensional array with a matching dimension as the target range, we can simply call a set values function. If the dimensions didn’t match we would get an error.&lt;/p&gt;

&lt;p&gt;The complete code for the processing looks like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const processData = (data) =&amp;gt; {
  const target = getDataRange()
  const rows = createArrayWithThreeColumns(target.getNumRows())

  data.forEach((value, index) =&amp;gt; {
    // business logic - e.g. ignore invalid dates
    if(new Date(value.date) == 'Invalid Date') return

    rows[index] = [value.date, value.name, JSON.stringify(value.counties)]
  })

  target.setValues(rows)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Note: I can see potential issues here - for example the target range could have less rows that the data we get from the API call. It would be good to write another test for that and refactor the code to make it more robust, but that is outside of the scope of this article. Feel free to comment and ask for a more detailed deep dive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final test
&lt;/h2&gt;

&lt;p&gt;Great!&lt;/p&gt;

&lt;p&gt;Let’s go back to the main test and run it. Is it going to pass now?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fejgq3pxi9dmytcueb1lf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fejgq3pxi9dmytcueb1lf.png" alt="Passing test"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes! Well done!&lt;/p&gt;

&lt;p&gt;For convenience, it's useful to create a function which runs all the tests to ensure that the whole solution is still working. This is something that a good IDE could do for us, unfortunately Google Apps Script doesn't provide this out-of-the-box.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const test_runAll = () =&amp;gt; {
  test_getAndProcessData() 
  test_createArrayWithThreeColumns()
  test_clearRange()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We can go back to the spreadsheet and try this again with different inputs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Frjeeynhqt680n8fp9sub.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Frjeeynhqt680n8fp9sub.png" alt="Manual test"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;In this article we covered how to test &lt;strong&gt;Google Apps Script&lt;/strong&gt; which gets data from an external API and stores it into &lt;strong&gt;Google Sheets&lt;/strong&gt;. We have quality testable code, applied single responsibility principle, and written unit tests for some functions and integration tests for others, using mocks instead of real services.&lt;/p&gt;

&lt;p&gt;Thanks for reading! &lt;/p&gt;

&lt;p&gt;You can also view this as a video on my &lt;a href="https://www.youtube.com/watch?v=Y7QROeICOyA" rel="noopener noreferrer"&gt;YouTube Channel&lt;/a&gt;&lt;/p&gt;

</description>
      <category>googleappsscript</category>
      <category>automation</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
