DEV Community

Cover image for Here’s how to test arrays
Daniel Irvine 🏳️‍🌈
Daniel Irvine 🏳️‍🌈

Posted on

Here’s how to test arrays

If I give you a function that returns an array of objects, how do you test the returned value? How many unit tests do you need? One, two, three? What are their descriptions?

In this post I’ll show you how I do it.

It follows on from my post last week about how to test dependencies. It’s written in Ruby but it applies to any dynamic language, including JavaScript. (Statically typed languages don’t necessarily need these types of test).

These posts are all about systemizing your testing.

Testing gets kind of boring once you have a system for it. But boring is good. Boring produces consistency. It means you aren’t getting stuck, or having to think too much.

Every time I have to test a returned array, I use this exact same approach.

Test 1. The right number of elements are returned

Imagine we’re testing a filter function that takes a source array of objects and a string to match on. Our tests will define the behavior of the function.

RSpec.describe "#filter" do
  let(:source) do
    [
      { id: 123, name: "Daniel", diet: :vegan, pet: :cat },
      { id: 234, name: "Danny", diet: :anything, pet: :goldfish },
      { id: 345, name: "Jane", diet: :vegetarian, pet: nil }
    ]
  end

  it "returns all elements matching the name parameter" do
    result = filter(source, "Dan")
    expect(result.length).to eq 2
    expect(result[0][:id]).to eq 123
    expect(result[1][:id]).to eq 234
  end

end

Here my test list has 3 items: you can normally prove everything with just items. No need for a longer list.

Now let’s look at how we might make that test pass.

def filter(source, name)
  source.select { |s| s[:name].include?(name) }
end

Test 2. That it returns the correct form of object

In the second test you want to ensure you test that you get all the properties that you’re expecting. Occassionally this test isn’t necessary:

it "returns all the right information" do
  result = filter(source, "Dan")
  expect(result.first).to eq source[0]
end

This test already passes, so we don’t need to write it. But if you aren’t returning the exact item from the source, then write out the value you expect instead:

it "returns all the right information" do
  result = filter(source, "Dan")
  expect(result.first).to eq({
    name: "Daniel",
    pet: :cat
  })
end

Now we actually have to write code to make that pass:

def filter(source, name)
  source
    .select { |s| s[:name].include?(name) }
    .map { |s| s.except(:diet) }
end

Test 3. That they’re in the right order

Sometimes, you care about the order that elements are returned. For this we need a new source list, again with at least three elements:

let(:unsorted_source) do
    [
      { id: 123, name: "Daniel" },
      { id: 234, name: "Dan" },
      { id: 345, name: "Danny" }
    ]
end

it "orders results alphabetically" do
  result = filter(unsorted_source, "Dan")
  expect(result[0][:name]).to eq("Dan")
  expect(result[1][:name]).to eq("Daniel")
  expect(result[2][:name]).to eq("Danny")
end

The elements must be in an unsorted order: neither ascending nor descending order. Otherwise, you could make this test pass simply by returning the same array or by calling reverse rather than sort_by.

And here’s the final result:

def filter(source, name)
  source
    .select { |s| s[:name].include?(name) }
    .map { |s| s.except(:diet) }
    .sort_by { |s| s[:name] }
end

Modification 1: Join tests 1 and 3

If I know I’m going to order my tests, I’ll join 1 and 3 together.

it "returns all elements matching the name, in alphabetical order" do
  result = filter(unsorted_source, "Dan")
  expect(result[0][:id]).to eq 234
  expect(result[1][:id]).to eq 123
  expect(result[2][:id]).to eq 345
end

Or perhaps you’d rather use map:

it "returns all elements matching the name, in alphabetical order" do
  result = filter(unsorted_source, "Dan")
  expect(result.map { |r| r[:id] }).to match_array [234, 123, 345]
end

And that’s all there is to it! 🎉

Top comments (0)