How to Generate Unit Tests with AI for Python and JavaScript

How to Generate Unit Tests with AI for Python and JavaScript
About the Author

Sam Na writes practical guides on AI-assisted productivity, reliable automation, and testing workflows that make digital systems easier to maintain. RoutineOS focuses on turning AI capabilities into useful processes that readers can understand, run, and verify.

Author: Sam Na Contact: seungeunisfree@gmail.com Published and updated: October 20, 2026
AI Unit Testing · Python · JavaScript

You can generate unit tests with AI in minutes, but a file full of passing tests does not automatically mean your code is reliable. ChatGPT, Claude, Gemini, and GitHub Copilot can help you build pytest and Jest test cases for ordinary inputs, empty data, invalid values, and unexpected conditions. The important part is making sure those tests check the behavior your program actually needs, rather than simply repeating what the existing code already does.

Imagine using AI to build a small automation that cleans up your task list every morning. It works when you try three sample tasks, so you connect it to your regular workflow. A few days later, an imported task has no title, another contains unexpected spacing, and a third has a missing field. Your automation produces incomplete results even though the original demonstration looked perfect.

You could ask an AI assistant to repair each problem as it appears. A better approach is to test the rules before those unusual inputs reach your daily routine. A unit test checks one small piece of program behavior against a result you have already defined. It can tell you whether a function returns the expected value, raises the right error, or calls a dependency with the correct information.

An effective AI unit test generator should produce test cases that challenge your code. A valuable test is not the one that merely passes today; it is the one that would reveal a meaningful mistake if the implementation changed.

AI is useful here because it can suggest scenarios you might overlook, translate plain-language requirements into test functions, and adapt tests to a framework you already use. The limitation is that an assistant may also invent requirements, write assertions that are too weak, or generate tests that cannot run in your project. Those mistakes can create false confidence unless you inspect the results.

This guide uses two practical tools: pytest for Python and Jest for JavaScript. We will create small, independent examples, write prompts that ask for meaningful tests, run the generated code, and examine the quality of the assertions. You can apply the same method whether your code comes from an AI assistant, an online tutorial, or your own work.

A test should describe what correct behavior looks like, not merely confirm that the current implementation behaves consistently.

Define what your tests should prove

The most common weakness in AI-generated unit tests appears before the first line of test code. You give an assistant a function and ask it to produce tests without explaining what that function is supposed to accomplish. The assistant reads the implementation and often assumes that its current behavior is correct.

That approach can preserve a bug instead of exposing it. Suppose a function accidentally treats a blank task title as valid. An assistant that uses the function itself as the specification may generate a test that expects the blank title to be accepted. The test passes, but it protects the wrong rule.

Start with behavior, not the current output

Before asking AI to generate tests, write down the function's purpose in ordinary language. Identify the inputs it accepts, the output it should return, and the situations in which it should refuse to continue. These statements form a behavior contract: a short description of what the function promises to do.

Consider a function that cleans task titles imported from a spreadsheet. You want it to accept text, remove extra spacing, and return a readable title. A title containing only whitespace should be rejected. A missing value or a number should also be rejected because those values are not valid titles in this example.

These rules are easier to test than a vague instruction such as "make sure the task cleaner works." Each rule can become an independent test case with an input and a clear expected outcome.

Normal behavior
A readable task title stays readable

A valid title such as "Plan weekly review" should retain its words and return the same text.

Normalization
Extra whitespace is removed

Text containing repeated spaces or line breaks should become a title with single spaces between words.

Invalid content
Blank titles are rejected

An empty string or a string containing only whitespace should produce a clear validation error.

Invalid type
Non-text inputs are rejected

Values such as None, an integer, or a list should not be silently converted into task titles.

Separate happy paths from edge cases

The happy path is the ordinary situation in which a user supplies valid input and receives the intended result. For the task-title function, the happy path is a normal string with readable words. It is the simplest place to start, but testing only that case leaves several important assumptions untouched.

Edge cases describe valid or invalid inputs near the limits of the function's behavior. These might include an empty string, a very long title, repeated spaces, different character sets, or a missing value. You do not need to test every imaginable string. Focus on conditions that could realistically appear in the data your automation processes.

Think about where the information comes from. A text field might return an empty string. A spreadsheet import might contain blank cells. An API might omit a field. A pasted title might contain tabs or line breaks. Each source introduces different risks, and the tests should reflect those risks.

Give each test one meaningful reason to exist

A test is more useful when its name explains the behavior it checks. Compare "test_function_3" with "test_rejects_blank_task_title." The second name immediately tells you what failed if the test turns red. It also helps someone reviewing your code understand the requirement without opening a long conversation with an AI assistant.

Avoid generating dozens of cases that all exercise the same branch with nearly identical inputs. Ten tests using different ordinary task titles may provide less protection than four carefully chosen tests covering a normal title, unusual whitespace, blank input, and an invalid type.

A useful way to judge a proposed test is to ask: what realistic mistake would cause this test to fail? If you cannot name one, the test may add more maintenance work than protection.

Key Takeaway

Define the intended behavior before generating test code. Include ordinary inputs, realistic boundaries, and invalid values, then make every test responsible for a specific rule.

Prepare your project and write better AI test prompts

Once you know what the code should do, give your AI assistant enough context to produce tests that fit your actual project. A test generated for the wrong language version, module format, or framework may look reasonable but fail before it reaches the function you want to check.

You can use ChatGPT, Claude, Gemini, or GitHub Copilot to draft unit tests. The general approach is similar across these tools: provide the relevant code, specify the testing framework, describe expected behavior, and ask for cases that challenge the implementation. Where an IDE assistant can see the current file, confirm that it has the correct context rather than assuming it inspected your entire project.

Prepare a clean testing environment

Keep application code and test code in separate files. This allows you to review the tests without mixing them into the function being tested. It also makes it easier to run the suite repeatedly whenever the implementation changes.

For Python, pytest can discover test functions in appropriately named test files and evaluate ordinary assert statements. The official pytest getting-started guide provides installation and first-test instructions. A virtual environment is a practical way to keep testing dependencies separate from other Python projects.

python -m venv .venv

# Activate the environment for your shell,
# then install pytest:

python -m pip install -U pytest

# Verify that pytest is available:

python -m pytest --version

For JavaScript, Jest is commonly installed as a development dependency in an existing Node.js project. The official Jest getting-started guide covers installation, test files, and the commands used to run them. The examples in this article use CommonJS modules to avoid requiring additional module or transpilation configuration.

# Run inside an existing Node.js project:

npm install --save-dev jest

# Run Jest directly:

npx jest --runInBand

If you are starting an entirely new JavaScript folder, initialize the project with npm before installing packages. If your application uses ECMAScript modules, TypeScript, or a framework with its own test configuration, tell the AI about that environment and follow the framework's supported setup. Do not copy a CommonJS example into an incompatible module configuration without adapting it.

Give AI the code, the rules, and the framework

A weak request is "write tests for this code." It leaves the assistant to infer the contract, choose its own framework, and decide how much of the project to change. You may receive a long suite containing incorrect import paths, invented dependencies, or assumptions that were never part of your requirements.

A stronger request limits the scope. Specify pytest or Jest, identify the function and file name, describe the accepted inputs, and provide the failure conditions. Ask the assistant not to modify the production function while generating its first set of tests. That separation makes the resulting test suite easier to evaluate independently.

Copy-Ready Prompt: Generate Unit Tests from a Behavior Contract
Generate unit tests for the function below. Language: [PYTHON OR JAVASCRIPT] Testing framework: [PYTEST OR JEST] Source file: [FILE NAME] Test file: [TEST FILE NAME] Intended behavior: [DESCRIBE THE REQUIRED BEHAVIOR] Valid inputs: [LIST REPRESENTATIVE VALID INPUTS] Invalid inputs: [LIST VALUES THAT MUST BE REJECTED] Boundary conditions: [LIST EMPTY, MISSING, OR UNUSUAL INPUTS] Requirements: 1. Use the testing framework I specified. 2. Test observable behavior, not implementation details. 3. Include normal, boundary, and invalid cases. 4. Name each test by the behavior it protects. 5. Do not change the production function. 6. Do not invent additional dependencies. 7. Identify any requirements that are unclear. 8. Provide the command needed to run the tests. 9. Do not claim the tests passed unless I provide real execution results. Source code: [PASTE SANITIZED CODE]

Use AI suggestions to expand coverage, not invent requirements

After generating the first suite, ask the assistant what conditions are still missing. This second pass is often more valuable than asking for a larger batch of tests immediately. The model can compare the written requirements with the existing test cases and suggest gaps without rewriting everything.

Review the suggestions before converting them into executable tests. For example, AI might assume task titles have a maximum length, cannot contain emoji, or must be written in English. Those restrictions may sound reasonable, but they are not part of our example's contract. Adding tests for unsupported assumptions would change the product's intended behavior.

1
Choose the framework
Specify pytest for the Python example or Jest for the JavaScript example, including the project's module format.
2
Share the function and its contract
Provide the smallest relevant source file, accepted inputs, expected outputs, and invalid cases.
3
Request one independent test file
Keep generated assertions separate from production logic so that they can be inspected before execution.
4
Review missing or invented assumptions
Reject tests based on rules you never approved and add cases for behaviors that remain untested.

Do not give an AI assistant real passwords, private customer records, production API credentials, or unnecessary personal information merely to generate unit tests. Use synthetic test inputs and sanitized code.

Key Takeaway

Good AI-generated tests begin with correct project context and a written behavior contract. Specify the framework, preserve the source code, and reject test cases based on invented product rules.

Generate Python unit tests with pytest

Python is a practical language for personal automations because it can process files, manipulate text, organize data, and communicate with other services. Those everyday tasks often depend on input quality. A function may work with the author's original sample but behave differently when a spreadsheet cell is blank or a file contains an unexpected value.

Our first example is a deliberately small task-title cleaner. Keeping the production function simple makes it easier to see which behaviors the generated tests cover and which assumptions remain untested.

Create a small function with a clear contract

Save the following function in a file named task_labels.py. It accepts a string, collapses repeated whitespace into single spaces, and returns the cleaned title. A non-string input produces TypeError, while a string that becomes empty after whitespace removal produces ValueError.

# task_labels.py

def clean_task_label(raw):
    if not isinstance(raw, str):
        raise TypeError("Task label must be text")

    label = " ".join(raw.split())

    if not label:
        raise ValueError("Task label cannot be blank")

    return label

Notice that this function does not remove punctuation, change capitalization, or impose a title-length limit. Those behaviors were never requested. The tests should therefore focus on the rules that actually exist: valid text, normalized spacing, blank inputs, and unsupported types.

If an AI assistant proposes additional cases involving forbidden punctuation or automatic capitalization, you can discuss those as possible requirements. Do not add them as assertions until the application's intended behavior has been decided.

Generate parameterized tests for valid inputs

One normal input cannot represent every useful variation. You also want to know whether the function handles leading spaces, repeated spaces, and embedded line breaks. These cases share the same basic expectation: return the normalized title.

pytest provides @pytest.mark.parametrize, which lets one test function run with several separate input and expected-output pairs. Its behavior is described in the official pytest parametrization documentation. This is especially useful when AI produces many related examples that should follow the same assertion.

# test_task_labels.py

import pytest
from task_labels import clean_task_label


@pytest.mark.parametrize(
    "raw, expected",
    [
        (
            "Plan weekly review",
            "Plan weekly review",
        ),
        (
            "  Review   weekly  plan ",
            "Review weekly plan",
        ),
        (
            "Send\nreport",
            "Send report",
        ),
    ],
)
def test_valid_task_labels(raw, expected):
    assert clean_task_label(raw) == expected

Each input has an expected result written directly in the test. The function must return that result for the test to pass. If the implementation stops collapsing line breaks, for example, the third case will reveal the difference between actual and expected output.

The expected values are important. An AI-generated test that simply asserts the result is not None would be far weaker. It could pass even if the function returned the wrong title. Whenever an exact output is part of the contract, test the exact output rather than an unrelated property.

Add exception tests and run the complete file

The same function also needs tests for invalid data. An empty string, spaces, and tabs should all fail because they do not contain a usable task title. Values such as None and integers should fail for a different reason: they are not text.

pytest offers pytest.raises to verify that a specific exception is produced. You can also check the relevant portion of the error message. This helps distinguish deliberate validation from an unrelated exception that happens elsewhere in the code.

# Add these tests to test_task_labels.py


@pytest.mark.parametrize(
    "raw",
    ["", "  ", "\t\n"],
)
def test_blank_task_labels(raw):
    with pytest.raises(
        ValueError,
        match="cannot be blank",
    ):
        clean_task_label(raw)


@pytest.mark.parametrize(
    "raw",
    [None, 42, ["Plan"]],
)
def test_non_text_task_labels(raw):
    with pytest.raises(
        TypeError,
        match="must be text",
    ):
        clean_task_label(raw)

Place the source file and the test file in the same small project directory for this example. From that directory, run pytest with your selected Python interpreter. Using python -m pytest helps ensure the command runs through that interpreter rather than another pytest executable elsewhere on your machine.

python -m pytest -q test_task_labels.py

The combined example contains three valid-input cases, three blank-input cases, and three invalid-type cases. The code shown here was checked with pytest, and all nine parameterized cases passed in that run. This result applies to the displayed implementation and tests, not to a different function that an AI assistant might generate for your project.

3 Valid input cases Normal text, repeated spaces, and an embedded line break.
3 Blank input cases Empty text, spaces, and whitespace-only content.
3 Invalid type cases None, an integer, and a list instead of a string.

When you adapt these tests to an existing project, pay attention to the import path and file layout. A generated test may refer to a module name that does not exist in your repository. An import error is not proof that the function failed its behavior contract; it may mean the test environment or file structure needs correction.

Key Takeaway

pytest makes it straightforward to turn a behavior contract into executable checks. Use parameterized tests for related inputs, verify intended exceptions, and confirm exact outputs rather than relying on broad assertions.

Generate JavaScript unit tests with Jest

JavaScript is commonly used in browser interfaces, Node.js scripts, and automations that process information from web services. Even small functions can have several behavioral branches. A function that builds a list of completed tasks, for example, must decide how to handle unfinished records, empty arrays, and incomplete task objects.

Jest gives you readable assertions and a convenient way to group related tests. The most useful AI-generated tests make those assertions reflect the intended data rules, not assumptions borrowed from unrelated applications.

Create a function that filters completed tasks

Our JavaScript example receives an array of task objects and returns the trimmed titles of tasks marked as completed. Unfinished tasks are ignored. An empty array returns an empty array. A completed task without a usable title produces an error, because the automation should not silently publish an incomplete summary.

Save the function below as tasks.js. The example uses CommonJS exports so it can be imported directly from the Jest test file in a basic Node.js project.

// tasks.js

function completedTaskTitles(tasks) {
  if (!Array.isArray(tasks)) {
    throw new TypeError("tasks must be an array");
  }

  return tasks
    .filter(task => task && task.done === true)
    .map(task => {
      if (
        typeof task.title !== "string" ||
        !task.title.trim()
      ) {
        throw new TypeError(
          "completed tasks need a non-empty title"
        );
      }

      return task.title.trim();
    });
}

module.exports = { completedTaskTitles };

The distinction between unfinished and completed records matters. An unfinished task does not need to appear in the completed-task summary. A completed task, however, must contain a readable title because its title will be used in the resulting list.

These rules give the AI clear expectations. Instead of asking whether the function runs, ask it to verify selection, ordering, trimming, empty results, invalid inputs, and preservation of the source data.

Use Jest assertions for observable results

Jest's toEqual matcher is useful when comparing arrays and objects by their contents. By contrast, toBe is generally used for primitive equality or reference identity. Choosing the appropriate matcher matters because a test can fail for reasons unrelated to the function's actual behavior.

Save these examples in tasks.test.js. The first case checks both task selection and whitespace trimming. The second and third cases verify that the function returns an empty array when nothing is completed.

// tasks.test.js

const { completedTaskTitles } = require("./tasks");


describe("completedTaskTitles", () => {

  test("returns trimmed titles of completed tasks", () => {
    const tasks = [
      { title: " Write outline ", done: true },
      { title: "Draft article", done: false },
      { title: " Review notes ", done: true },
    ];

    expect(completedTaskTitles(tasks)).toEqual([
      "Write outline",
      "Review notes",
    ]);
  });

  test("returns an empty array for no tasks", () => {
    expect(completedTaskTitles([])).toEqual([]);
  });

  test("ignores tasks that are not completed", () => {
    const tasks = [
      { title: "Draft article", done: false },
    ];

    expect(completedTaskTitles(tasks)).toEqual([]);
  });

});

Notice that the first test checks the order of the output as well. The function preserves the order of completed tasks in the input. If someone later changes the implementation to sort titles alphabetically, this test can reveal the change when the expected order differs from the sorted order.

The tests do not need an actual task-management account or live API. They use small artificial inputs that represent the behavior the function promises. That makes them fast to run and easier to understand when one fails.

Test invalid inputs and avoid accidental mutation

A useful unit test suite should also check what happens when the caller supplies something other than an array. It should confirm that a completed record without a valid title does not quietly disappear from the summary.

Another useful requirement is that filtering the task list must not modify the original data. This matters when other parts of an automation reuse the same array. A helper that sorts or modifies records in place could create surprising behavior elsewhere.

// Add these tests inside the same describe block.

test("rejects a non-array input", () => {
  expect(() => completedTaskTitles("tasks"))
    .toThrow(TypeError);
});

test("rejects a completed task without a title", () => {
  expect(() => completedTaskTitles([
    { done: true },
  ])).toThrow("non-empty title");
});

test("does not change the original task objects", () => {
  const originalTask = Object.freeze({
    title: " Read report ",
    done: true,
  });

  const tasks = Object.freeze([originalTask]);

  expect(completedTaskTitles(tasks)).toEqual([
    "Read report",
  ]);

  expect(originalTask.title).toBe(" Read report ");
});

For this example, the additional tests belong inside the existing describe block in tasks.test.js. The first three tests and these additional tests can then be run together. Alternatively, Jest also supports top-level test calls if you choose to place a test outside the group.

npx jest tasks.test.js --runInBand

The core function's output, empty-array behavior, error paths, and non-mutation behavior were checked separately using Node.js assertions. The Jest examples use documented Jest syntax, but their output should still be verified in the reader's own installed Jest environment. Do not present a predicted green result as a test run that has already happened.

Weak assertion
Only checks that something was returned

An assertion such as expect(result).toBeDefined() may pass even when the completed-task list is wrong.

Strong assertion
Checks the result the user actually needs

toEqual verifies the exact expected titles, their order, and the absence of unfinished tasks.

Key Takeaway

Use Jest to verify meaningful JavaScript behavior, including array contents, error conditions, and unintended data changes. Strong assertions protect the function's contract, not merely its ability to return a value.

Test edge cases, exceptions, and asynchronous behavior

Once the ordinary inputs are covered, AI can help you identify scenarios that are easy to overlook. This is where test generation becomes more useful than producing repetitive examples. The goal is to expose assumptions about data shape, validation, timing, external services, and failure handling.

Not every function needs every category of test. A pure text-cleaning function does not need network mocks. A script that sends a reminder, however, needs more than a correct message string; it should also confirm what information is sent and how failures are handled.

Ask AI for cases that could change the outcome

Begin with the source of your data. If information comes from forms, consider blank fields, invalid formats, and values outside allowed limits. If it comes from a CSV file, consider missing columns and unexpected encodings. If it comes from an API, consider missing properties, null values, and failed requests.

Boundary conditions should follow the actual requirement. If a function accepts between one and five items, tests for zero, one, five, and six items are more valuable than dozens of random mid-range values. The same principle applies to dates, text length, numerical limits, and allowed categories.

Normal input
Expected data arrives in the expected form

Confirm the normal result before testing unusual conditions. The baseline makes later failures easier to interpret.

Empty input
No data is available

Decide whether an empty list should return an empty result, skip an action, or produce a validation error.

Invalid input
Data has the wrong type or structure

Test a missing field, unsupported type, or malformed value according to the function's explicit contract.

Boundary input
A value lies at or beyond an allowed limit

Check the exact boundary and one value outside it to detect incorrect comparisons or off-by-one mistakes.

Isolate network calls, files, and other side effects

A unit test should usually avoid sending real messages, changing production files, or updating live accounts. Those operations introduce external conditions that make the result harder to interpret. A failing network request could break the test even when the local function behaves correctly.

For JavaScript, Jest provides mock functions through jest.fn(). They can record how a dependency was called and return controlled results without contacting a real service. The official Jest mock-functions guide explains this approach.

Imagine a function that sends a prepared weekly summary through a supplied messaging function. The example below separates the action from the actual delivery service, so a test can provide a fake sender and inspect its calls.

// digest.js

async function sendWeeklyDigest(send, message) {
  if (typeof message !== "string" || !message.trim()) {
    throw new TypeError("message must contain text");
  }

  await send(message);
  return true;
}

module.exports = { sendWeeklyDigest };

Instead of using a real email or messaging account, the test creates a mock sender that resolves successfully. It then checks whether the intended message was passed to the dependency. The test remains local and produces no real notification.

// digest.test.js

const { sendWeeklyDigest } = require("./digest");

test("sends the requested digest message", async () => {
  const send = jest.fn().mockResolvedValue({
    id: "fake-message-1",
  });

  const result = await sendWeeklyDigest(
    send,
    "Weekly planning summary"
  );

  expect(result).toBe(true);
  expect(send).toHaveBeenCalledTimes(1);

  expect(send).toHaveBeenCalledWith(
    "Weekly planning summary"
  );
});

This checks the local function's interaction with its dependency, not the performance of a real messaging provider. A live-service integration test would answer different questions, such as whether authentication works or the message actually reaches the intended destination. Keep those purposes distinct.

Test rejected promises and temporary files safely

Asynchronous code introduces another requirement: the test must wait for the operation to finish. If an AI-generated Jest test starts a promise without awaiting or returning it, the test might finish before the assertion or rejection occurs. That can produce a misleading pass.

For a messaging function, you may want to verify that a delivery failure is not silently reported as success. Jest provides rejected-promise matchers and mock rejection helpers, making it possible to test this condition without causing a real outage.

// Add this test to digest.test.js

test("reports a failed delivery", async () => {
  const send = jest.fn().mockRejectedValue(
    new Error("Delivery unavailable")
  );

  await expect(
    sendWeeklyDigest(send, "Weekly summary")
  ).rejects.toThrow("Delivery unavailable");

  expect(send).toHaveBeenCalledTimes(1);
});

Python file-based automations need a similar separation from production data. pytest provides a tmp_path fixture that supplies a temporary directory for a test. A file-writing function can use that directory rather than a real documents folder. You can also use controlled fixtures or mocks for other dependencies.

Ask the AI to generate tests around the dependency boundary: what arguments are passed, how success is recognized, and what happens if the dependency fails. Avoid asking it to create real service accounts or use personal credentials simply to prove that a small function behaves correctly.

Copy-Ready Prompt: Generate Safe Edge-Case and Mock Tests
Review this function and its existing unit tests. Identify the most important missing cases involving: - Empty or missing data - Invalid types and values - Boundary conditions - Dependency failures - Asynchronous completion - Unintended side effects Generate only tests that follow the behavior contract I provide. Use: [PYTEST OR JEST] For external dependencies: - Use mocks, stubs, or temporary resources. - Do not access live services. - Do not send messages or modify production files. - Await asynchronous operations correctly. - Explain what each added test would catch. If a required behavior is unspecified, ask for clarification rather than inventing a rule. Function: [PASTE FUNCTION] Existing tests: [PASTE TESTS] Behavior contract: [PASTE REQUIREMENTS]

Never treat a test that sends a real message, deletes a user file, charges an account, or changes a production record as a harmless unit test. Replace external actions with controlled substitutes unless a separate, authorized integration test specifically requires the real service.

Key Takeaway

The best additional tests target realistic boundaries and failure conditions. Use mocks and temporary resources to keep unit tests independent of live systems, and make asynchronous tests wait for the operation they are checking.

Check whether AI-generated tests are trustworthy

Generating tests is only the first half of the work. The second half is deciding whether those tests provide useful evidence. AI can produce a large suite with attractive names and technically valid syntax while leaving important behavior unchecked.

A passing test means that the implementation satisfied the assertions it actually executed. It does not mean the assertions represented the correct requirements, covered every important branch, or exercised the code you thought they exercised.

Inspect assertions instead of counting passing tests

Read every generated assertion and ask what it establishes. A test that only checks whether a function returns an array can pass while the array contains the wrong items. A test that merely checks whether a promise resolves can pass even if the message contents are incorrect.

A strong assertion checks the relevant result or interaction. For a title cleaner, compare the exact cleaned string. For a completed-task selector, compare the expected array contents. For a sender, check both the call count and the argument sent through the mocked dependency.

Be suspicious of tests that repeat values taken from the current output without comparing them with the written contract. Those tests can turn an implementation mistake into an apparently official requirement.

Weak AI-generated test
The function returns something

The test checks that a result exists or that no exception occurred, even when the function has an exact expected result.

Meaningful unit test
The result satisfies an explicit rule

The test compares the actual output with an independently defined expectation and fails when that rule is violated.

Check whether tests can detect a deliberately introduced mistake

A practical way to challenge a test suite is to make a controlled, temporary mistake in a disposable copy of the production code. Change one behavior that the tests are supposed to protect, run the tests, and confirm that at least one relevant assertion fails. Then restore the correct implementation.

For the task-title function, you could temporarily remove whitespace normalization. The test using repeated spaces should then fail. If it still passes, inspect whether the test executed the correct function and whether its expected value was meaningful.

This idea resembles mutation testing, where small code changes are used to evaluate the sensitivity of a test suite. You do not need a specialized mutation-testing package to understand the basic principle. For personal projects, one carefully chosen temporary change can reveal a weak assertion.

1
Save the correct working version
Preserve the source code and ensure the existing test suite runs before introducing any temporary change.
2
Make one controlled behavioral change
In a disposable copy, alter a rule that a specific test is supposed to protect.
3
Run the relevant tests
Confirm that the expected test fails for the intended reason, rather than because of a syntax or import problem.
4
Restore the implementation
Remove the temporary change and verify that the original tests pass again.

Treat coverage reports as evidence, not a quality certificate

Test coverage measures which parts of the code were exercised during a test run, according to the coverage tool's configuration. It can reveal untested lines and branches, but it cannot determine whether your expected outputs are correct.

Jest can collect coverage information with its coverage option. For the simple JavaScript project, you can run the tests with a coverage report after installing Jest and configuring the project appropriately.

npx jest --coverage --runInBand

For Python, coverage reporting can be added with a compatible coverage tool such as pytest-cov. It is a separate plugin rather than a built-in replacement for pytest. Install and configure it only when that extra reporting is useful for your project.

A high coverage percentage can coexist with weak assertions. A test might execute every branch of a function while checking only that the function returns without crashing. Conversely, a small suite with clear assertions may protect the most consequential behavior even before coverage is extensive.

PASS Assertion satisfied The observed result matched what that particular test checked.
FAIL Investigation required The mismatch may involve code behavior, test expectations, configuration, or dependencies.
COVER Execution measured Coverage indicates exercised code, not guaranteed correctness.

Also watch for tests that pass only under one machine or execution order. A test depending on the current time, random values, shared state, or a live service may produce inconsistent results. AI-generated tests should control those conditions whenever possible so that failures have a clear and reproducible cause.

Do not ask AI to make a failing test pass by changing the expected result until it matches the current implementation. Determine whether the requirement, the implementation, or the test setup is wrong before accepting any change.

Key Takeaway

Test quality depends on meaningful assertions, independent expectations, and the ability to detect real mistakes. Passing counts and coverage percentages are useful signals, but neither proves that the underlying behavior is correct.

Build a practical unit testing routine

Once you have a useful set of tests, make running them a normal part of working on your scripts. You do not need an elaborate development process to benefit. A small personal automation can become much easier to maintain when its important behaviors are checked whenever you change the code.

The most manageable approach is to begin with the functions that matter most: the ones that transform data, decide whether an action should happen, or prepare information for external services. These functions often have clear inputs and outputs, making them suitable starting points for unit tests.

Run focused tests while editing and the full suite before reuse

While working on one function, running its specific test file provides fast feedback. After the change works, run the broader relevant suite. This helps catch unexpected effects in other parts of the project without requiring a long manual review after every small edit.

Keep test commands easy to find. A short project README or a package script can record how to execute them. If several people or multiple AI tools contribute code, the commands should not depend on remembering a particular chat conversation.

Python
Run the relevant pytest file

Use python -m pytest -q with the test file path to check the function you changed.

JavaScript
Run the relevant Jest file

Use npx jest with the test file name to check the corresponding module.

Before integration
Run the wider test suite

Check that the change does not conflict with other verified behaviors in the same project.

After new requirements
Update expectations deliberately

Modify tests when the approved behavior changes, not merely because the implementation produces a different result.

Use GitHub Copilot or another assistant to extend an existing suite

AI becomes more useful for testing when it can see your existing conventions. If you already have pytest fixtures, Jest mocks, established naming patterns, or utility helpers, include those in the relevant context. The assistant is more likely to generate tests that fit the project instead of introducing another style or dependency.

GitHub's official guide to writing tests with Copilot describes generating unit and integration tests and reviewing the resulting suggestions. Depending on the supported editor and current interface, test-generation actions may also be available from the active file or selected code.

You can use a similar review process with ChatGPT, Claude, or other code-capable assistants. Provide the function, the existing tests, and the missing behavior. Request a small, reviewable addition rather than an entirely regenerated test folder.

Copy-Ready Prompt: Extend an Existing Test Suite
Review the existing unit tests for this function. Do not replace the entire test file. Compare the behavior contract with the current assertions and identify the three most consequential missing cases. For each proposed test: 1. Name the behavior it protects. 2. Explain what realistic bug it could catch. 3. Use the existing testing framework. 4. Reuse existing fixtures and helpers where appropriate. 5. Avoid new dependencies. 6. Do not alter production code. 7. Do not invent unspecified requirements. 8. Return only the new tests and explain where to insert them. Framework: [PYTEST OR JEST] Behavior contract: [PASTE CONTRACT] Source code: [PASTE FUNCTION] Existing tests: [PASTE TEST FILE]

Decide which tests deserve permanent maintenance

More tests are not always more useful. A personal automation with a hundred repetitive assertions can be harder to maintain than a compact suite that protects the important rules. Keep tests that detect realistic failures, clarify requirements, or guard behaviors you are likely to change.

Remove unnecessary duplication when several tests exercise exactly the same behavior. Use parameterization to combine related examples without losing their individual expected results. Keep exceptional cases separate when they represent different rules or need a different explanation.

When a requirement genuinely changes, update the contract first. Then adjust the tests to describe the newly approved behavior and change the implementation accordingly. This order prevents your test suite from becoming a collection of historical assumptions that no longer match the workflow.

The tests reflect documented behavior rather than assumptions inferred from existing code.
Normal inputs, realistic boundaries, and invalid values have appropriate assertions.
Test names explain the behavior being protected.
The tests use the project's actual framework, module format, and dependency configuration.
External services and consequential side effects are replaced with controlled test substitutes where appropriate.
Asynchronous tests wait for promises to resolve or reject.
Test results come from actual execution rather than a chatbot's prediction.
At least the most important assertions have been checked for their ability to detect an intentional mistake.
The complete relevant suite runs successfully before a change is incorporated into the regular workflow.
Key Takeaway

Make unit testing a repeatable habit rather than a one-time AI generation task. Keep the suite focused, extend it when requirements change, and run the actual tests before relying on updated automation code.

Frequently asked questions

Q1. Can AI generate unit tests for existing Python code?
Yes. AI assistants can draft pytest tests from Python functions and their intended behavior. Provide the source code, expected outputs, invalid-input rules, and project structure. Review the generated assertions before running them because the assistant may otherwise assume that the current implementation is correct.
Q2. What is the difference between pytest and Jest?
pytest is a testing framework for Python, while Jest is a JavaScript testing framework. Both can check expected outputs and failures, but they use different syntax, configuration, and project environments. Choose the framework that matches the language and setup of the code you are testing.
Q3. Can ChatGPT generate pytest tests automatically?
ChatGPT can draft pytest test functions, parameterized cases, and exception assertions from supplied requirements. Whether it can execute those tests depends on the tools available in the particular session. Run the generated test file with your project's Python environment to verify the actual result.
Q4. How do I generate unit tests with GitHub Copilot?
In a supported IDE, open or select the relevant code and ask Copilot to generate tests. Specify pytest or Jest, the important edge cases, and the expected behavior. Review the resulting changes, confirm import paths, and execute the tests locally. Available commands and interfaces depend on the editor and product version.
Q5. How many unit tests should AI generate for one function?
There is no universally correct number. Start with tests for the normal behavior, important boundaries, invalid inputs, and meaningful failure paths. A small set of distinct, well-designed assertions is more useful than many repetitive tests that check the same condition.
Q6. Why do AI-generated tests sometimes pass when the code is wrong?
The tests may contain weak assertions, incorrect expected values, or assumptions copied from the implementation. They may also skip the branch that contains the bug. Compare test expectations with the behavior contract and consider a controlled temporary code change to see whether important tests can detect a mistake.
Q7. How can I test asynchronous JavaScript code without sending real requests?
Inject a fake dependency or use Jest mock functions to replace the external operation. Await the promise in the test and assert the call arguments, success result, or rejected error. This keeps the unit test focused on local behavior without sending actual messages or modifying live services.
Q8. Does 100% test coverage mean AI-generated code is bug-free?
No. Coverage reports measure which code was exercised under the configured measurement rules. They do not prove that test expectations are correct or that every important condition was considered. Strong assertions, representative inputs, and independent review are still necessary.

Conclusion: make AI-generated tests prove something useful

AI can make it much easier to begin testing a Python script or JavaScript function. You can turn a written behavior contract into pytest and Jest examples, cover unusual inputs that did not appear during development, and build a reusable set of checks without starting every test from a blank file.

The value comes from the decisions behind those tests. You need to know which outcomes are correct, which inputs are invalid, and which external actions must remain isolated. When those rules are clear, an AI assistant can help translate them into executable assertions. When the rules are missing, a longer generated suite may simply reinforce assumptions that were never verified.

Begin with one function from an automation you already use. Write down its expected behavior, generate a small pytest or Jest suite, and run that suite yourself. Examine each assertion and try one controlled change that should cause a test to fail. Once the tests demonstrate that they can detect a meaningful mistake, you have a stronger foundation for making future code changes safely.

Your Next Step: Test One Real Automation Function

Choose a Python or JavaScript function that handles data you regularly depend on. Define its normal result, one empty-input case, and one invalid-input case. Ask an AI assistant to generate tests using pytest or Jest, then run the tests in your own environment. Keep the cases that genuinely protect the function's intended behavior.

About Sam Na

Sam Na writes RoutineOS guides about practical AI workflows, digital productivity, and personal automation. His articles focus on helping readers understand the behavior of their tools, evaluate AI-generated output, and build systems that remain useful beyond the first successful demonstration.

Sam Na · RoutineOS seungeunisfree@gmail.com
A Quick Note Before You Apply These Examples

This guide provides general information and educational code examples. The right testing approach depends on your project, dependencies, data, and the consequences of running the code. Test examples in a safe environment before using them in an important automation. For consequential deployments, check current official documentation and seek guidance from a qualified developer or security professional when appropriate.

References and Official Documentation
pytest Documentation — Get Started. Official instructions for installing pytest, creating a test, and running a test session.
pytest Documentation — How to Parametrize Fixtures and Test Functions. Official guidance on running the same test with multiple input and expected-output combinations.
Jest Documentation — Getting Started. Official setup instructions for JavaScript unit tests, test files, assertions, and command-line execution.
Jest Documentation — Mock Functions. Official documentation for substituting controlled functions, inspecting calls, and testing asynchronous dependencies.
GitHub Docs — Writing Tests with GitHub Copilot. Official examples of generating and reviewing unit and integration tests with Copilot.
Previous Post Next Post