# An AI agent skill for writing tests in the `nushell` repo.

> Source: <https://gist.github.com/cptpiepmatz/2e73779a4a4493c6339d68215eb9aa83>
> Published: 2026-07-19 10:44:47+00:00

| name | nushell-testing | ||||||
|---|---|---|---|---|---|---|---|
| description | Use when writing or refactoring Nushell Rust tests that use nu_test_support, NuTester, Playground, rstest, deps, plugins, CompleteResult, or when replacing old nu!/nu_with_plugins! tests. | ||||||
| license | MIT | ||||||
| compatibility | opencode | ||||||
| metadata |
|

Use this skill when writing new tests or refactoring old tests in the Nushell repository. The goal is to use the modern `nu_test_support`

infrastructure directly, keep tests small and explicit, and avoid subprocess-style testing unless the behavior under test truly crosses the `nu`

binary boundary.

The most important rule: do not use `nu!`

in new tests. Do not introduce `nu!`

, `nu_with_plugins!`

, `nu_with_std!`

, or `nu_repl_code`

. Some of these APIs may still exist for compatibility in older branches, but new and refactored tests should use `test()`

and `NuTester`

instead.

Prefer in-process tests. `test()`

returns a `NuTester`

that runs Nushell code in-process against the current crate code. This is faster, more deterministic, and avoids stale `nu`

binaries.

Assert values, not text rendering. If a command returns a list, record, bool, int, table, duration, or structured error, assert that structured value directly. Do not add `to json`

, `to nuon`

, `to text`

, `str join`

, or manual output collapsing just to make assertion easier.

Make required external artifacts explicit. If a test needs a compiled `nu`

binary, use `#[deps(NU)]`

. If it needs a plugin, use `#[deps(NU_PLUGIN_EXAMPLE)]`

or the appropriate plugin dependency. Do not call `add_nu_to_path()`

in new code.

Keep tests local and readable. Avoid tiny helper functions that hide the testing infra. Prefer a few explicit lines in the test body, `rstest`

cases for repeated shapes, and constants only for large shared data or setup snippets.

Return `Result`

. Most Nushell tests should be `fn test_name() -> Result`

, where `Result`

comes from `nu_test_support::prelude::*`

. Let `?`

propagate `TestError`

from `NuTester`

and assertion helpers.

Use the newest guidance over older examples. Older refactors may use `add_nu_to_path()`

; newer test infra replaces that with `#[deps(NU)]`

.

Most test files should start with the prelude:

```
use nu_test_support::prelude::*;
```

Add specific imports when needed:

```
use nu_protocol::Signals;
use pretty_assertions::assert_matches;
use rstest::rstest;
```

Do not import `test_value!`

, `test_record!`

, or `test_table!`

again; those `test_*!`

macros are already provided by `nu_test_support::prelude::*`

.

Do not import the old macros for new tests:

```
// Bad
use nu_test_support::nu;
use nu_test_support::{nu, nu_repl_code};
use nu_test_support::nu_with_plugins;
```

Why: even when `nu`

is still re-exported by compatibility modules or the prelude on some branches, that is not permission to use it in new tests. The direction is to remove these macros from tests.

For a single snippet and a single expected value, chain `test().run(...).expect_value_eq(...)`

:

``` php
use nu_test_support::prelude::*;

#[test]
fn can_average_range() -> Result {
    test().run("0..5 | math avg").expect_value_eq(2.5)
}
```

This is better than a `nu!`

test because it compares a real `Value`

converted through `IntoValue`

, not collapsed stdout text.

Do not write this:

``` js
use nu_test_support::nu;

#[test]
fn can_average_range() {
    let actual = nu!("0..5 | math avg");
    assert_eq!(actual.out, "2.5");
}
```

Use a local variable named `code`

only for long or multiline snippets:

```
use nu_test_support::prelude::*;

#[test]
fn format_filesize_respects_float_precision_for_fractional_values() -> Result {
    let code = "
        $env.config = ($env.config | upsert float_precision 5)
        1024B | format filesize kB
    ";

    test().run(code).expect_value_eq("1.02400 kB")
}
```

Guidelines for multiline snippets:

Use `let code = "..."`

or `let code = r#"..."#`

when the snippet is long or multiline.

For short snippets, pass a one-line string directly to `run(...)`

or `run_with_data(...)`

.

Keep Nushell code readable; do not compress everything into one line unless it is genuinely clearer.

If the expected output is a multiline Rust string, prefer `indoc::indoc!`

.

Use `cwd`

on the tester when the snippet reads files relative to a fixture directory:

``` php
use nu_test_support::prelude::*;

#[test]
fn reads_cargo_sample() -> Result {
    let code = r#"
        open cargo_sample.toml
        | get package
        | format pattern "{name} has license {license}"
    "#;

    test()
        .cwd("tests/fixtures/formats")
        .run(code)
        .expect_value_eq("nu has license ISC")
}
```

`cwd`

accepts repo-relative paths and sandbox paths. Prefer it over `cd`

in the snippet when the test setup itself should start in a specific directory.

`run`

extracts the result into any type implementing `FromValue`

. `expect_value_eq`

accepts any type implementing `IntoValue`

.

Never write `run::<T>(...)`

or `run_with_data::<T>(...)`

. If you extract a value, let the left-hand side define the type with `let value: T = test().run(...) ?`

. If you call `expect_value_eq`

, `expect_shell_error`

, `expect_parse_error`

, or similar, the helper already fixes the needed type.

Simple expected values should be raw Rust values when they are homogeneous or scalar:

``` php
#[test]
fn filters_ints() -> Result {
    test()
        .run("[3, 1, 2] | sort")
        .expect_value_eq([1, 2, 3])
}
```

Use `test_value!`

, `test_record!`

, and `test_table!`

for mixed, nested, record, or table-like data. Prefer `test_value!`

for nested structures instead of nesting `test_record!`

inside `test_record!`

:

``` php
use nu_test_support::prelude::*;

#[test]
fn insert_at_end_of_list() -> Result {
    test()
        .run("[1, 2, 3] | insert 3 abc")
        .expect_value_eq(test_value!([1, 2, 3, "abc"]))
}
php
use nu_test_support::prelude::*;

#[test]
fn inserts_into_nested_record() -> Result {
    test()
        .run("{a: {}} | insert a.b.c 0")
        .expect_value_eq(test_value!({
            a: { b: { c: 0 } }
        }))
}
php
use nu_test_support::prelude::*;

#[test]
fn insert_uses_enumerate_index() -> Result {
    let code = "
        [[a]; [7] [6]]
        | enumerate
        | insert b {|el| $el.index + 1 + $el.item.a }
        | flatten
    ";

    test().run(code).expect_value_eq(test_table![
        ["index", "a", "b"];
        [0, 7, 8],
        [1, 6, 8],
    ])
}
```

Prefer these forms over serialized output:

```
// Bad unless serialization itself is under test
#[test]
fn insert_at_end_of_list() -> Result {
    test()
        .run("[1, 2, 3] | insert 3 abc | to json --raw")
        .expect_value_eq(r#"[1,2,3,"abc"]"#)
}
php
// Good
#[test]
fn insert_at_end_of_list() -> Result {
    test()
        .run("[1, 2, 3] | insert 3 abc")
        .expect_value_eq(test_value!([1, 2, 3, "abc"]))
}
```

Exceptions are real rendering and serialization tests, such as `to json`

, `to nuon`

, `table`

, or `to text`

, where the string representation is the behavior under test.

When assertions need Rust logic, extract a typed value:

``` php
#[test]
fn splits_empty_path() -> Result {
    let outcome: bool = test().cwd("tests").run("echo '' | path split | is-empty")?;
    assert!(outcome);
    Ok(())
}
```

If the test only verifies that a command succeeds and returns a value of a certain type, bind to `_`

:

``` php
#[test]
fn is_terminal_accepts_stdin_flag() -> Result {
    let _: bool = test().run("is-terminal --stdin")?;
    Ok(())
}
```

If you only care that a command succeeds and returns any value:

``` php
#[test]
fn help_does_not_error() -> Result {
    let _: Value = test().run("overlay hide --help")?;
    Ok(())
}
```

If the command should return `nothing`

, use `let ()`

or `expect_value_eq(())`

:

``` php
#[test]
fn definition_compiles() -> Result {
    let () = test().run("def helper [] { 'ok' }")?;
    Ok(())
}
```

Use `run_with_data`

when the test needs external data. This keeps the Nushell snippet stable and avoids unsafe or noisy `format!`

use:

``` php
use nu_test_support::prelude::*;

#[test]
fn reads_input_record() -> Result {
    let input = test_record! {
        "name" => "nu",
        "license" => "MIT",
    };

    test()
        .run_with_data("$in | format pattern '{name} has license {license}'", input)
        .expect_value_eq("nu has license MIT")
}
```

Prefer this over injecting values into source with `format!`

:

``` php
// Bad when only the value changes
#[test]
fn uppercases_name() -> Result {
    let name = "nu";
    test()
        .run(format!("'{name}' | str upcase"))
        .expect_value_eq("NU")
}
php
// Good
#[test]
fn uppercases_name() -> Result {
    let name = "nu";
    test()
        .run_with_data("$in | str upcase", name)
        .expect_value_eq("NU")
}
```

`format!`

is fine when you are intentionally generating Nushell syntax, such as an option flag or a large code fixture, and the generated code remains obvious.

Use one mutable `NuTester`

when later snippets depend on definitions, environment changes, overlays, or variables from earlier snippets:

``` php
#[test]
fn use_main_def_env() -> Result {
    let mut tester = test();

    let () = tester.run(r#"module spam { export def --env main [] { $env.SPAM = "spam" } }"#)?;
    let () = tester.run("use spam")?;
    let () = tester.run("spam")?;

    tester.run("$env.SPAM").expect_value_eq("spam")
}
```

Use `run_multiple`

when the test intentionally needs to run several pipelines after each other on the same tester and only the final value matters:

``` php
#[test]
fn new_overlay_from_const_name() -> Result {
    let commands = [
        "const mod = 'spam'",
        "overlay new $mod",
        "overlay list | last | get name",
    ];

    test().run_multiple(commands).expect_value_eq("spam")
}
```

Prefer explicit repeated `run`

calls when intermediate results matter, when some steps are setup with `nothing`

output, or when the source-unit boundaries are important for understanding the test:

``` php
#[test]
fn use_main_def_env() -> Result {
    let mut tester = test();
    let () = tester.run(r#"module spam { export def --env main [] { $env.SPAM = "spam" } }"#)?;
    let () = tester.run("use spam")?;
    let () = tester.run("spam")?;
    tester.run("$env.SPAM").expect_value_eq("spam")
}
```

Do not use `nu_repl_code`

. If the test wants REPL-like separate source units, use `run_multiple`

for a simple sequence where only the final value matters, or repeated `tester.run(...)`

calls when intermediate steps matter.

Be deliberate about source-unit boundaries:

If behavior must occur in one parse/source unit, put it in one `code`

block and run once.

If behavior must persist across source units, use repeated `run`

calls on one mutable tester.

If scenarios are independent, use separate `test()`

calls or `rstest`

cases.

Example of independent scenarios using fresh testers:

``` php
#[test]
fn module_main_not_found() -> Result {
    let mut tester = test();
    let () = tester.run("module spam {}")?;
    tester
        .run("use spam main")
        .expect_error_code_eq("nu::parser::export_not_found")?;

    let mut tester = test();
    let () = tester.run("module spam {}")?;
    tester
        .run("use spam [ main ]")
        .expect_error_code_eq("nu::parser::export_not_found")
}
```

Use `Playground::setup`

when the test needs a sandboxed filesystem:

```
use nu_test_support::{fs::Stub::EmptyFile, prelude::*};

#[test]
fn rm_deletes_file() -> Result {
    Playground::setup("rm_deletes_file", |dirs, sandbox| {
        sandbox.with_files(&[EmptyFile("delete-me.txt")]);

        test()
            .cwd(dirs.test())
            .run("rm delete-me.txt; 'delete-me.txt' | path exists")
            .expect_value_eq(false)
    })
}
```

Rules for playground tests:

Return the `Playground::setup`

result directly from the test.

Use `test().cwd(dirs.test())`

when the snippet should run in the sandbox.

Use `FileWithContentToBeTrimmed`

for indented file contents.

When the Rust side needs to read a file, do not use `file_contents(path)`

. Use `std::fs::read_to_string(path)?`

or `fs::read_to_string(path)?`

if `std::fs`

is imported. `io::Error`

converts into `TestError`

, so this works naturally in tests that return `Result`

:

```
use std::fs;
use nu_test_support::{fs::Stub::FileWithContent, prelude::*};

#[test]
fn writes_expected_file() -> Result {
    Playground::setup("writes_expected_file", |dirs, sandbox| {
        sandbox.with_files(&[FileWithContent("input.txt", "hello")]);

        let () = test()
            .cwd(dirs.test())
            .run("open input.txt | str upcase | save output.txt")?;

        let contents = fs::read_to_string(dirs.test().join("output.txt"))?;
        assert_eq!(contents, "HELLO");
        Ok(())
    })
}
```

Do not create a playground when no filesystem behavior is being tested.

Do not write this shape:

``` php
// Bad
#[test]
fn bad_playground_shape() -> Result {
    Playground::setup("case", |dirs, sandbox| {
        sandbox.with_files(&[EmptyFile("x")]);
        test().cwd(dirs.test()).run("ls")?;
        Ok(())
    });

    Ok(())
}
```

Write this instead:

``` php
#[test]
fn good_playground_shape() -> Result {
    Playground::setup("case", |dirs, sandbox| {
        sandbox.with_files(&[EmptyFile("x")]);
        let _: Value = test().cwd(dirs.test()).run("ls")?;
        Ok(())
    })
}
```

Use `rstest`

when several tests have the same shape and only data changes:

```
use nu_test_support::prelude::*;
use rstest::rstest;

#[rstest]
#[case::null("null", ())]
#[case::true_("true", true)]
#[case::false_("false", false)]
fn top_level_values_from_json(#[case] json: &str, #[case] expected: impl IntoValue) -> Result {
    test()
        .run_with_data("from json", json)
        .expect_value_eq(expected)
}
```

This is better than a loop inside one test because every case is reported independently.

When combining `rstest`

with `#[deps]`

, `#[env]`

, `#[exp]`

, `#[serial]`

, or other harness attributes, explicitly use the harness test macro:

```
use nu_test_support::prelude::*;
use rstest::rstest;

#[rstest]
#[case::child_lib_dirs("$env.NU_LIB_DIRS | describe", "list<string>")]
#[case::child_plugin_dirs("$NU_PLUGIN_DIRS | describe", "list<string>")]
#[nu_test_support::test]
#[deps(NU)]
fn child_process_defaults(#[case] child_code: &str, #[case] expected: impl IntoValue) -> Result {
    test()
        .run_with_data("let child_code; nu -n -c $child_code", child_code)
        .expect_value_eq(expected)
}
```

For per-case dependencies, follow nearby repository style and place the dependency attribute next to the case it belongs to.

Prefer structured error assertions.

If the exact error code is enough, use `expect_error_code_eq`

:

``` php
#[test]
fn insert_past_end_of_list() -> Result {
    test()
        .run("[1, 2, 3] | insert 5 abc")
        .expect_error_code_eq("nu::shell::access_beyond_end")
}
```

If the variant or fields matter, extract the error and match it:

```
use nu_test_support::prelude::*;
use pretty_assertions::assert_matches;

#[test]
fn insert_nested_path_into_empty_list_errors_without_underflow() -> Result {
    let err = test().run("[] | insert 0.0 1").expect_shell_error()?;
    assert_matches!(err, ShellError::AccessEmptyContent { .. });
    Ok(())
}
```

Parse and compile errors have dedicated helpers:

```
use nu_protocol::Type;
use nu_test_support::prelude::*;
use pretty_assertions::assert_matches;

#[test]
fn parse_function_signature_switch_is_bool() -> Result {
    let err = test()
        .run("def foo [--bar] { let baz: int = $bar }")
        .expect_parse_error()?;

    assert_matches!(err, ParseError::TypeMismatch(Type::Int, Type::Bool, _));
    Ok(())
}
```

Use `ShellErrorExt`

for generic or labeled errors when needed:

``` php
#[test]
fn glob_without_match_reports_missing_file() -> Result {
    let err = test().run("ls root3*").expect_shell_error()?;
    let msg = err.generic_msg()?;
    assert_contains("file or folder not found", msg);
    Ok(())
}
```

Avoid this old style:

``` js
// Bad
#[test]
fn insert_past_end_of_list() {
    let actual = nu!("[1, 2, 3] | insert 5 abc");
    assert!(actual.err.contains("too large"));
}
```

Use `expect_value_eq`

for Nushell values.

Use `assert_contains(needle, haystack)`

for positive contains checks.

Use `pretty_assertions::assert_eq`

or `pretty_assertions::assert_str_eq`

for large Rust-side values or rendered strings when it improves diffs.

Use `assert_matches!`

for error variants and enum shapes.

Avoid `assert!(haystack.contains(needle))`

; it gives weaker diagnostics than `assert_contains`

.

Avoid `assert_eq!(actual.out, ...)`

because new tests should not produce `actual.out`

in the first place.

Use `assert_contains_not(needle, haystack)`

for negative containment checks. Prefer it over hand-written `assert!(!haystack.contains(needle))`

because it matches the positive `assert_contains`

style and gives a consistent failure message:

``` php
#[test]
fn overlay_list_does_not_contain_hidden_overlay() -> Result {
    let names: String = test().run("overlay list | get name | str join ' '")?;
    assert_contains_not("spam", names);
    Ok(())
}
```

Most tests should not spawn the `nu`

binary. Spawn it only when testing behavior that exists at the process boundary, such as CLI flags, script-file execution, process exit behavior, or child process environment defaults.

When spawning `nu`

, declare `#[deps(NU)]`

:

```
use nu_test_support::prelude::*;

#[test]
#[deps(NU)]
fn source_file_relative_to_file() -> Result {
    let result: CompleteResult = test()
        .cwd("tests/parsing/samples")
        .run("nu -n source_file_relative.nu | complete")?;

    assert_eq!(result.exit_code, 0);
    assert_eq!(result.stdout.trim(), "5");
    assert_eq!(result.stderr, "");
    Ok(())
}
```

Use `CompleteResult`

for `complete`

output instead of indexing a record:

``` php
#[test]
#[deps(NU)]
fn child_nu_reports_missing_script() -> Result {
    let result: CompleteResult = test().run("nu -n missing-script.nu | complete")?;

    assert_ne!(result.exit_code, 0);
    assert_contains("file_not_found", result.stderr);
    Ok(())
}
```

Do not use `add_nu_to_path()`

in new tests:

``` php
// Bad
#[test]
fn child_version() -> Result {
    test()
        .add_nu_to_path()
        .run("nu -n -c 'version | get version'")
        .expect_value_eq(env!("CARGO_PKG_VERSION"))
}
php
// Good
#[test]
#[deps(NU)]
fn child_version() -> Result {
    test()
        .run("nu -n -c 'version | get version'")
        .expect_value_eq(env!("CARGO_PKG_VERSION"))
}
```

Do not call test binaries through `nu --testbin ...`

. That route is deprecated for tests. Declare the testbin dependency and call the binary directly:

``` php
// Bad
#[test]
#[deps(NU)]
fn bad_testbin_call() -> Result {
    test()
        .run("nu --testbin cococo a b c")
        .expect_value_eq("a b c")
}
php
// Good
#[test]
#[deps(TESTBIN_COCOCO)]
fn calls_testbin_directly() -> Result {
    test()
        .run("cococo a b c")
        .expect_value_eq("a b c")
}
```

Use the appropriate `TESTBIN_*`

constant from the prelude, such as `TESTBIN_COCOCO`

, `TESTBIN_MEOW`

, or `TESTBIN_REPEATER`

. Add `NU`

only if the behavior under test specifically requires a child Nushell process in addition to the testbin.

Plugin tests should not use `nu_with_plugins!`

. Declare plugin dependencies with `#[deps]`

and call plugin commands directly in-process:

```
use nu_test_support::prelude::*;

#[test]
#[deps(NU_PLUGIN_EXAMPLE)]
fn call_to_json() -> Result {
    test()
        .run("[42] | example call-decl 'to json' {indent: 4}")
        .expect_value_eq("[\n    42\n]")
}
use nu_test_support::prelude::*;

#[test]
#[deps(NU_PLUGIN_EXAMPLE)]
fn plugin_receives_config() -> Result {
    let code = r#"
        $env.config = {
            plugins: {
                example: {
                    path: "some/path"
                    nested: { bool: true, string: "Hello Example!" }
                }
            }
        }
        example config
    "#;

    test().run(code).expect_value_eq(test_value!({
        path: "some/path",
        nested: {
            bool: true,
            string: "Hello Example!"
        }
    }))
}
```

If a plugin test specifically needs an actual child `nu`

process plus a plugin path, declare both dependencies and pass the plugin path as data:

```
#[test]
#[deps(NU, NU_PLUGIN_INC)]
fn plugin_process_exits_when_nushell_exits() -> Result {
    let pid: u32 = test().run_with_data(
        r#"nu -n --plugins $in -c "'2.0.0' | inc -m; (plugin list).0.pid" | into int"#,
        NU_PLUGIN_INC.path(),
    )?;

    let mut tester = test();
    let () = tester.run("sleep 500ms")?;
    let () = tester.run_with_data("let pid = $in", pid)?;
    tester
        .run("ps | where pid == $pid | is-empty")
        .expect_value_eq(true)
}
```

Use `#[serial]`

for plugin tests that check process identity, start/stop timing, or shared process state. Do not mark all plugin tests serial by default.

By default, `NuTester`

keeps `$env.PATH`

unset or minimal for deterministic tests.

Use `#[deps(NU)]`

for the Nushell binary.

Use plugin dependencies for plugins.

Use `#[deps(TESTBIN_X)]`

for test binaries and invoke them directly by name. Do not route testbins through `nu --testbin`

.

Use `inherit_path()`

only when the test intentionally relies on arbitrary system commands.

Use `inherit_rust_toolchain_env()`

for tests that intentionally spawn `cargo`

, `rustc`

, or `rustup`

.

Example:

``` php
#[test]
fn cargo_is_available_when_inheriting_rust_toolchain_env() -> Result {
    test()
        .inherit_rust_toolchain_env()
        .run("cargo --version | split row ' ' | get 0")
        .expect_value_eq("cargo")
}
```

Do not use `inherit_path()`

as a workaround for missing `#[deps]`

.

The custom harness supports attributes on tests:

`#[serial]`

runs tests sequentially. Use it for process-wide state, heavy IO contention, plugin PID lifecycle tests, or other unavoidable shared-state cases. Do not use global locks instead.

`#[env(KEY = "value")]`

sets process environment variables for a test group. Use this when engine setup or harness grouping depends on an environment variable.

`#[exp(OPTION)]`

enables an experimental option. Use it instead of setting experimental options manually in a `nu!`

call.

`#[deps(...)]`

declares binaries or plugins that must be built and made available to `test()`

.

Example:

```
use nu_experimental::REORDER_CELL_PATHS;
use nu_test_support::prelude::*;

#[test]
#[exp(REORDER_CELL_PATHS)]
fn update_table_cell_respects_reorder_option() -> Result {
    let code = "
        let a = [[foo]; [bar]]
        let b = ($a | update foo.0 'baz')
        $b.0.foo
    ";

    test().run(code).expect_value_eq("baz")
}
```

For locale-sensitive tests, use `#[env(NU_TEST_LOCALE_OVERRIDE = "en_US.UTF-8")]`

when the locale must affect harness/engine setup, or `test().locale_en()`

when configuring only the tester is enough.

For commands whose output is intentionally a string rendering, assert the string directly. Use `indoc!`

to keep expected output readable.

```
use indoc::indoc;
use nu_test_support::prelude::*;

#[test]
fn table_padding_zero() -> Result {
    test()
        .run_with_data(
            "
                let data = $in
                $env.config.table.padding = {left: 0, right: 0}
                $data | table --width=80
            ",
            test_table![
                ["a", "b", "c"];
                [1, 2, 3],
                [4, 5, [1, 2, 3]],
            ],
        )
        .expect_value_eq(indoc! {"
            ╭─┬─┬─┬──────────────╮
            │#│a│b│      c       │
            ├─┼─┼─┼──────────────┤
            │0│1│2│             3│
            │1│4│5│[list 3 items]│
            ╰─┴─┴─┴──────────────╯
        "})
}
```

Notes for rendering tests:

Use `run_with_data`

for complex input so the test focuses on rendering.

Use `ansi strip`

when colors are not the behavior under test.

When colors are the behavior under test, assert the ANSI string explicitly.

Use `pretty_assertions::assert_str_eq!`

when comparing large rendered strings extracted as `String`

.

Use this checklist when converting old tests.

- Remove
`use nu_test_support::nu`

,`nu_with_plugins`

, and`nu_repl_code`

imports. - Add
`use nu_test_support::prelude::*;`

and only specific extra imports that are needed. - Change
`fn test_name()`

to`fn test_name() -> Result`

. - Replace
`let actual = nu!(...)`

with`test().run(...)`

,`test().cwd(...).run(...)`

, or`let mut tester = test(); tester.run(...)`

. - Replace
`assert_eq!(actual.out, "...")`

with`.expect_value_eq(...)`

or typed extraction. - Replace
`assert!(actual.err.contains(...))`

with`.expect_error_code_eq(...)`

,`.expect_shell_error()?`

,`.expect_parse_error()?`

, or`.expect_compile_error()?`

. - Replace plugin macro usage with
`#[deps(NU_PLUGIN_...)]`

and direct plugin commands. - Replace
`add_nu_to_path()`

with`#[deps(NU)]`

. - Replace
`nu --testbin name ...`

with`#[deps(TESTBIN_NAME)]`

and direct`name ...`

calls. - Replace
`nu_repl_code`

with`run_multiple`

or a mutable`NuTester`

and repeated`run`

calls, depending on which is clearer. - Replace serialization-only assertions with structured values unless serialization is the feature under test.
- Use
`rstest`

when the refactor exposes several same-shaped tests. - Keep the test body explicit rather than moving a small sequence into a helper.

Example refactor:

``` js
// Old
use nu_test_support::nu;

#[test]
fn insert_into_list() {
    let actual = nu!("[1, 2, 3] | insert 1 abc | to json -r");
    assert_eq!(actual.out, r#"[1,"abc",2,3]"#);
}
php
// New
use nu_test_support::prelude::*;

#[test]
fn insert_into_list() -> Result {
    test()
        .run("[1, 2, 3] | insert 1 abc")
        .expect_value_eq(test_value!([1, "abc", 2, 3]))
}
```

Error refactor:

``` js
// Old
#[test]
fn list_unknown_long_flag() {
    let actual = nu!("ls --full-path");
    assert!(actual.err.contains("Did you mean: `--full-paths`?"));
}
// New
use nu_protocol::ParseError;
use nu_test_support::prelude::*;
use pretty_assertions::assert_matches;

#[test]
fn list_unknown_long_flag() -> Result {
    let err = test().run("ls --full-path").expect_parse_error()?;
    assert_matches!(
        err,
        ParseError::UnknownFlag(_, _, _, help) if help == "Did you mean: `--full-paths`?"
    );
    Ok(())
}
```

Plugin refactor:

``` js
// Old
#[test]
fn call_reduce() {
    let result = nu_with_plugins!(
        cwd: ".",
        plugin: ("nu_plugin_example"),
        "[1 2 3] | example call-decl 'reduce' {fold: 10} { |it, acc| $it + $acc }"
    );
    assert_eq!("16", result.out);
}
php
// New
#[test]
#[deps(NU_PLUGIN_EXAMPLE)]
fn call_reduce() -> Result {
    test()
        .run("[1 2 3] | example call-decl 'reduce' {fold: 10} {|it, acc| $it + $acc}")
        .expect_value_eq(16)
}
```

Multiple source-unit refactor:

```
// Acceptable when only the final value matters
#[test]
fn new_overlay_from_const_name() -> Result {
    let commands = [
        "const mod = 'spam'",
        "overlay new $mod",
        "overlay list | last | get name",
    ];

    test().run_multiple(commands).expect_value_eq("spam")
}
// Prefer this when intermediate steps or assertions matter
#[test]
fn new_overlay_from_const_name() -> Result {
    let mut tester = test();
    let () = tester.run("const mod = 'spam'")?;
    let () = tester.run("overlay new $mod")?;
    tester.run("overlay list | last | get name").expect_value_eq("spam")
}
```

Do not use `nu!`

for new tests.

Do not use `nu_with_plugins!`

.

Do not use `nu_repl_code`

.

Do not use `add_nu_to_path()`

.

Do not call testbins via `nu --testbin`

; use `#[deps(TESTBIN_...)]`

and call them directly.

Do not assert `actual.out`

, `actual.err`

, or `actual.status`

from `nu!`

output.

Do not write `run::<T>(...)`

or `run_with_data::<T>(...)`

; annotate the left-hand binding or use the expectation helpers.

Do not depend on `NU`

just because a test used to spawn `nu`

; first ask whether the behavior can be tested in-process.

Do not depend on a plugin or binary without `#[deps(...)]`

.

Do not serialize values to JSON, NUON, markdown, text, or joined strings only to assert them.

Do not use `Playground::setup`

when no filesystem behavior is needed.

Do not use `file_contents(path)`

; use `std::fs::read_to_string(path)?`

instead.

Do not emulate serialization or isolation with global mutexes; use `#[serial]`

when serial execution is truly required.

Do not create helper functions just to wrap `test().run`

or hide a small command sequence.

Do not manually set process environment with `std::env::set_var`

; use `#[env]`

or `test().env(...)`

.

Do not manually enable experimental options in the snippet or old macro options; use `#[exp]`

.

Do not import `test_value`

, `test_record`

, or `test_table`

from `nu_protocol`

when `nu_test_support::prelude::*`

is already imported; the prelude provides the `test_*!`

macros.

Use `test().run(code).expect_value_eq(expected)`

for one snippet and one expected value.

Use `let mut tester = test(); tester.run(...)`

for shared state across source units.

Use `test().run_multiple(commands)`

for a simple sequence of pipelines where only the last output is asserted.

Use one multiline `code`

block for behavior that must parse or execute in one source unit.

Use `run_with_data`

when external Rust data is the input.

Use `Playground::setup`

plus `.cwd(dirs.test())`

for sandboxed files.

Use `rstest`

for same-shaped cases.

Use `expect_error_code_eq`

when the code is the contract.

Use `expect_shell_error`

, `expect_parse_error`

, or `expect_compile_error`

when the error shape matters.

Use `#[deps(NU)]`

plus `CompleteResult`

when testing a child `nu`

process.

Use `#[deps(TESTBIN_...)]`

when testing commands that need a testbin, then call the testbin directly.

Use `#[deps(NU_PLUGIN_...)]`

when testing plugin commands in-process.

Use `#[serial]`

only for unavoidable shared state, timing-sensitive process lifecycle, or heavy IO conflicts.

Above all, keep the test explicit, type-aware, and free of `nu!`

.
