Testing and Structured Data
Learning Objectives
- You understand why predictable data structures are useful in small CLI programs.
- You can parse and produce JSON in JavaScript.
- You can write simple Deno tests using standard assertion helpers.
Structured data
Programs become easier to test when they exchange data in predictable structures. In this course, that usually means arrays, objects, and JSON. The idea is simple: if a function returns a structure whose fields are stable and intentional, later code can inspect individual fields instead of trying to guess meaning from a long formatted string.
As an example, a text-analysis function might return the following kind of object:
{
totalWords: 42,
uniqueWords: 18,
topWords: [["hello", 5], ["world", 4]]
}
This is easier to test than a long formatted string, because each field can be checked directly.
That does not mean formatted strings are bad. A CLI program still needs readable output for people. The useful pattern is to keep the internal representation structured for as long as possible, and only format it into human-facing text near the end of the pipeline.
Parsing and serializing JSON
JavaScript includes built-in JSON support.
const data = JSON.parse('{"name":"Ada","role":"engineer"}');
console.log(data.role);
const text = JSON.stringify(data, null, 2);
console.log(text);
Structured JSON output becomes especially useful later when a model response is meant to feed into deterministic application logic.
It is also useful for debugging. If a program can print or save a result as JSON, you can inspect the exact structure that later code receives. That is often much easier than debugging formatted text with hidden assumptions.
Validation mindset
Not every piece of data should be trusted automatically. Even before using formal schema libraries, it is useful to validate the parts of a structure that matter.
const isValidGradeSummary = (value) => {
return typeof value === "object" &&
value !== null &&
typeof value.count === "number" &&
typeof value.average === "number";
};
This kind of small validation function is often enough to prevent obvious mistakes in small programs. A validation function does not need to be complicated to be useful. It only needs to encode the assumptions that later code depends on.
There are also libraries like Zod that make writing validation rules easier.
For example, if the rest of the program assumes that count and average are numbers, then that is exactly what should be checked. The goal is not to describe every possible detail of the data. The goal is to prevent invalid data from silently flowing into the next step.
Testing with Deno
Deno includes built-in testing support. For assertions, we can use @std/assert. We first add it to our project:
$ deno add @std/assert@1.0.15
This adds it to deno.json. After this, it can be imported in the code.
import { assertEquals } from "@std/assert";
const countWords = (text) => {
return text.split(/\s+/).filter((word) => word.length > 0).length;
};
Deno.test("countWords counts words separated by spaces", () => {
assertEquals(countWords("hello world"), 2);
});
Run tests with:
$ deno test
If a test reads files, writes a transcript, or inspects environment variables, Deno may require explicit flags such as --allow-read, --allow-write, or --allow-env.
A useful habit is to test the smallest meaningful unit of logic first. In a CLI project, this often means testing a pure helper function before testing the whole program. Pure functions are easier to test because the result depends only on the input, not on files, network access, or terminal interaction.
Test names are also part of documentation. A name such as "countWords counts words separated by spaces" is more informative than a vague label like "works correctly". Later, if the test fails, the name should help you understand what behavior broke.
Stubbing external dependencies
Some of the later course exercises need to interact with files, environment variables, or HTTP APIs. Those interactions should still be tested, but they do not have to contact real third-party services during testing.
One useful technique is stubbing. In Deno, @std/testing/mock can temporarily replace a function while a test runs. Suppose that a project has a helper called fetchJson that uses fetch internally. The test can then replace fetch with a controlled stub:
import { assertEquals } from "@std/assert";
import { stub } from "@std/testing/mock";
Deno.test("fetchJson returns parsed JSON", async () => {
using fetchStub = stub(globalThis, "fetch", () =>
Promise.resolve(
new Response(JSON.stringify({ title: "Stubbed title" }), { status: 200 }),
)
);
const data = await fetchJson("https://example.com/posts/1");
assertEquals(data.title, "Stubbed title");
});
This is valuable for two reasons. First, the test becomes deterministic: the result does not depend on network access, rate limits, or a remote provider being available. Second, the test can focus on the program logic that matters locally, such as request construction, parsing, validation, and fallback behavior.
The same idea also works for local module objects. If a program uses a repository object or a small service object, a test can stub one method on that object and observe how the rest of the program responds. In other words, testing still covers interactions between components, but the most unstable dependency can be replaced with a controlled test double.
As the course progresses, this becomes especially important for LLM applications. We want to test application logic thoroughly, but we do not want depend on real API keys or live model calls.
Stable output matters
Programs are easier to maintain when they produce outputs that are stable and intentional. If two functions both return objects of a similar shape, later code can rely on that shape. If a function sometimes returns a string, sometimes an object, and sometimes null, both testing and reuse become harder.
This principle will matter again in Part 3 when we ask LLMs to produce structured outputs that other parts of the program need to consume. The surrounding code becomes much safer when the application expects a small explicit structure, validates it, and only then proceeds.
The programming exercise for this chapter focuses entirely on writing tests. It is a good place to practice naming tests clearly, checking structured return values, and thinking about edge cases before the later API-facing chapters.