Note to self: Post regularly

Meanwhile, instead of writing posts, I taught coding boot camps for individuals striving to become skilled (and employed) as full-stack web developers.

This endeavour was mutually beneficial since it provided me the opportunity to dive deeply into JavaScript and the MERN stack.

Truth be told, I’m still struggling to fully embrace test-driven development so I thought it would be productive to write tests for a web snippet app inspired by a Pluralsight play-by-play course in which Lea Verou implements Conway’s Game of Life.

I took so much away from the course I felt it worth the cost subscribing for at least two months, maybe three. Perhaps the most significant take-away was the concept of not using any libraries (e.g. jQuery).

Now, I love jQuery as much as the next jQuery user, but getting by with a thin encapsulation of the raw DOM methods, i.e. rolling my own, was an enticing challenge. So, it will be the first thing I test.

The task of the element selector/creator interface (i.e. function) is to provide access to an element from the DOM. The jQuery implementation supports any valid CSS selection syntax for specifying the element(s) to proffer. It also accepts a value such as <div> to indicate the caller’s desire to create DOM elements afresh.

My implementation will not support psuedo-class selection and will return a NodeList (i.e. array) for multiple matching elements. The calling code will have to check if they get a list or a single element in return.

Unpacking that last paragraph results in the following criteria:

  1. input should be a valid CSS selector to indicate selecting an existing element
  2. input should be a valid HTML token to indicate creating a new element
  3. return an array of elements when provided a CSS selector resulting in multiple matches
  4. return a single element when the selector matches a single existing element
  5. return a single element when creating a new element
  6. handle invalid input gracefully and return null

The tests will include negative testing to ensure the graceful handling of invalid input. I’ll leave it up to product owner types to define what graceful means. For now I’ll just display a message indicating the input is invalid.

Since I explored the mocha/chai testing tools in a previous article, I’ve decided to use Jest this time around.

Here is the selection function I’ll be testing:

function $(s)  {
  // used to shorten lines
  // for concise display
  const dc = document.createElement
  const dq = document.querySelectorAll
  const c = console

  try {
    if(s.match(/<[a-zA-Z]+\w*>/)){
        // we need to create a new element
        return dc(s.substr(1, s.length - 2))
    }
    // must select (an) existing element(s)
    const elem = dq(s)
    if(elem && elem.length > 0) {
      return elem.length > 1 ? elem : elem[0]
    } else {
      c.log(`"${s}" did not match any elements.`)
      return null
    }
  } catch(x) {
    c.error(x.message)
    return null
  }
}

Typically I’d export this function from a module so that it can be employed by both the production code and the test suite. This is what I did when running the tests so please keep that in mind.

The tests need known markup, i.e. a control DOM, to work with. Jest is packaged with jsdom which allows me to run the tests in Node instead of having to use a browser.

The HTML I’m using to create the DOM looks like this:

<!DOCTYPE html>
<html><head><title>Test</title></head>
<body>
    <header><h1 class="class1">Test</h1></header>
    <main>
        <section id="section1">
            <article><p>Paragraph 1</p></article>
        </section>
        <section id="section2">
            <article><p>Paragraph 2</p></article>
        </section>
    </main>
    <footer><h2 class="class1">The End</h2></footer>
</body>
</html>

With that squared away, it’s on to verifying the requirements. Let’s go over them one-by-one.

The first requirement deals with selecting existing elements. There’s some ambiguity in the requirements and, ideally, I’d want to clarify this point. For the sake of this article, my function will support selecting existing elements by tag name, class value or id value. Let’s test selecting by tag name first:

test('test tag name selection', () => {
    // shorter lines
    const dg = document.getElementByTagName

    const m_elem = dg('main')[0]
    const s_elem = $('main')
    expect(s_elem).toBe(m_elem)
})

The control DOM contains a single <main> tag which allows the test to expect the strictly equals comparison to succeed. Specifically, the test uses the standard DOM selection mechanism getElementsByTagName to store the first (i.e. only) matching DOM element. It then uses the function under test to select the same, single element. Note that this test is verifying two aspects of the requirements: selecting by tag name and returning a single element for a single match. It might be useful to separate the testing of these requirements but I’ll leave that as an exercise for the reader.

The next two tests verify selecting by class and id values. The class selection will also verify returning an array of elements for multiple matches:

test('test class selection',
() => {
    // line length reduction
    const dc = document.getElementByClassName

    const c1Elems = dc('class1')
    const sElems = $('.class1')
    expect(sElems.length).toEqual(cElems.length)
})

test('test id selection',
() => {
    // line length reduction
    const dc = document.getElementById
    const s1 = dc('section1')
    const sElem = $('#section1')
    expect(sElem).toBe(s1)
})

Next I’ll test the creation of a new element:

test('test new element creation',
() => {
    // line length reduction
    const dg = document.getElementById
    const qs = document.querySelector
    const newP = $('<p>')
    const sa = newP.setAttribute

    expect(newP).not.toBeNull()
    sa('id', 'p2')
    newP.innerText = 'New P'

    const aDiv = qs('#section2 article ')
    aDiv.appendChild(newP)

    const pElem = dg('p2')
    expect(pElem.innerText).toEqual('New P')
})

This test verifies both the creation via the function under test and the suitability of that new element for inclusion in the DOM. Specifically, it expects the element to be created and also expects to be able to manipulate that element and add it to the existing document.

Lastly, I’ll write the negative tests:

test('test non-existing element',
() => {
    const noElem = $('form')
    expect(noElem).toBeNull()
})

test('test invalid selector',
() => {
    const xSel = $('|/@')
    expect(xSel).toBeNull()
})

test('test invalid element tag',
() => {
    const xElem = $('<sp ec !4 |>')
    expect(xElem).toBeNull()
})

All three of the negative tests expect the function to return null. Fortunately, the jest command line displays the graceful failure messages as well:

test runs

Now that my function’s requirements are validated with unit tests I can confidently continue development. Keeping in mind, of course, that requirement changes will necessitate additional tests. This is the way. And, as you can see from my example, unit tests routinely take more code than the function under test. They do save time overall, however.

Randy