Skip to main content

nu_test_support/
lib.rs

1#![expect(clippy::test_attr_in_doctest)]
2//! Test support for the Nushell crates.
3//!
4//! This crate provides tools for testing Nushell crates, including support for both unit and
5//! integration testing.
6//! It offers a [custom test harness](#custom-test-harness) to control the environment tests run in, along with
7//! [filesystem sandboxing](playground), utilities for
8//! [executing and asserting nushell scripts](tester), and additional general helper functionality.
9//!
10//! # Custom Test Harness
11//!
12//! Running tests in specific environments is difficult with the default built-in test harness,
13//! especially when it comes to serial execution, setting environment variables, or configuring
14//! global state.
15//! This crate provides a [custom test harness](harness) based on [kitest] to address these issues.
16//! It works for both unit and integration tests, and most crates in nushell are already set up to
17//! use it.
18//! The harness behaves similarly to the regular test harness, so getting started does not require
19//! special knowledge.
20//!
21//! ## Setup for Unit Tests
22//!
23//! In Cargo.toml of the crate:
24//! ```custom,{class=language-toml}
25//! [lib]
26//! harness = false # important part
27//! ```
28//! This disables the built-in test harness in your library and requires a `main` function to
29//! execute tests.
30//! You can simply import the provided entry point:
31//! ```
32//! #[cfg(test)]
33//! use nu_test_support::harness::main;
34//! ```
35//!
36//! ## Setup for Integration Tests
37//!
38//! In Cargo.toml of the crate:
39//! ```custom,{class=language-toml}
40//! [package]
41//! autotests = false # disable automatically found tests
42//!
43//! [[test]]
44//! name = "tests"         # whatever name fits here
45//! path = "tests/main.rs" # path to the main file
46//! harness = false        # disable the test harness
47//! ```
48//! This disables autotests, so all integration tests must be defined manually.
49//! All tests should live in the defined test binary as modules, and since the default harness is
50//! disabled, the provided harness must be used.
51//!
52//! ## Using `#[test]` Macro
53//!
54//! To use the provided test harness and have it discover tests, a new test macro setup is required.
55//! In the `main.rs` of the test:
56//! ```
57//! #[cfg(test)] // for unit tests, not required for integration tests
58//! #[macro_use]
59//! extern crate nu_test_support;
60//! ```
61//! This overrides the prelude macros with those from this crate, in particular the
62//! [`test`](harness::macros::test) macro.
63//! This allows test writers to keep using `#[test]` on test functions as usual.
64//!
65//! ## Configuring Test Environment
66//!
67//! When using the test harness, additional attributes are available that can be used together with
68//! `#[test]` to control how tests are executed.
69//!
70//! - `#[serial]`
71//!   Runs tests sequentially. This is useful when tests require significant
72//!   resources or interfere with each other when executed in parallel.
73//!
74//! - `#[env(FOO = "bar")]`
75//!   Sets environment variables for a specific test. The harness still
76//!   inherits the existing environment, but this allows overriding or adding
77//!   variables for individual tests.
78//!
79//! - `#[exp(nu_experimental::EXAMPLE)]`
80//!   Enables a specific [experimental option](nu_experimental) for a test.
81//!   It can also be explicitly disabled with
82//!   `#[exp(nu_experimental::EXAMPLE = false)]`.
83//!
84//! - `#[deps(NU)]`
85//!   Declares required binary dependencies for a test.
86//!   The harness ensures the binaries are built before the test runs and makes them available to
87//!   [`test()`]. Plugin dependencies, such as `#[deps(NU_PLUGIN_EXAMPLE)]`, are preloaded
88//!   automatically when the `plugin` feature is enabled.
89//!
90//! Tests with matching environment configurations or experimental settings are grouped together,
91//! allowing them to run in parallel where possible.
92//! Binary dependencies only affect the preparation phase, the harness builds dependencies for the
93//! filtered test set before execution starts.
94//!
95//! # Writing Integration Tests
96//!
97//! This crate provides the [`NuTester`](tester::NuTester) struct, which makes it easy to write
98//! integration tests that execute Nushell scripts.
99//! The main entry point is the [`test()`] function, which returns a `NuTester` instance
100//! preconfigured with all commands, relevant environment variables, and the standard library.
101//!
102//! Each execution group within the test harness receives its own freshly created tester instance.
103//! This ensures that environment variables and experimental options are properly isolated between
104//! tests.
105//!
106//! By running tests in-process instead of spawning a separate `nu` binary, tests can be
107//! significantly faster.
108//! This also improves iteration speed, since the binary does not need to be rebuilt before each
109//! run. Additionally, the initial `NuTester` setup is performed once and then cloned, reducing
110//! overhead across multiple tests.
111//!
112//! When writing integration tests, it is recommended to always import the [`prelude`] to avoid
113//! repeatedly importing common utilities.
114//! Input and output handling relies heavily on the [`IntoValue`](nu_protocol::IntoValue)
115//! and [`FromValue`](nu_protocol::FromValue) traits, making it easy to pass data into
116//! Nushell and extract values for assertions in a natural way.
117//!
118//! ## Simple Test Execution and Equality Assertion
119//!
120//! A basic pattern is to run a Nushell snippet and assert its output:
121//!
122//! ```
123//! # #[macro_use] extern crate nu_test_support;
124//! use nu_test_support::prelude::*;
125//!
126//! #[test]
127//! fn short_example() -> Result {
128//! #     unimplemented!()
129//! # }
130//! #
131//! # fn main() -> Result {
132//!     test()
133//!         .run("version | get version")
134//!         .expect_value_eq(env!("CARGO_PKG_VERSION"))
135//! }
136//! ```
137//!
138//! For improved readability, especially with longer pipelines, it can be
139//! helpful to store the script in a variable:
140//!
141//! ```
142//! # #[macro_use] extern crate nu_test_support;
143//! use nu_test_support::prelude::*;
144//!
145//! #[test]
146//! fn longer_example() -> Result {
147//! #     unimplemented!()
148//! # }
149//! #
150//! # fn main() -> Result {
151//!     let code = r#"
152//!         [a [b c]]
153//!         | flatten
154//!         | str join " "
155//!     "#;
156//!
157//!     test().run(code).expect_value_eq("a b c")
158//! }
159//! ```
160//!
161//! ## Pulling Data out of Test Run
162//!
163//! The [`run`](tester::NuTester::run) method of [`NuTester`](tester::NuTester) is commonly used
164//! together with [`expect_value_eq`](tester::TestResultExt::expect_value_eq) to compare the
165//! result of a script with a value that implements [`IntoValue`](nu_protocol::IntoValue).
166//!
167//! In cases where direct comparison is not convenient, `run` can also return values by converting
168//! them into a type that implements [`FromValue`](nu_protocol::FromValue).
169//! This makes it easy to extract data from Nushell and work with it in Rust.
170//!
171//! ```
172//! # #[macro_use] extern crate nu_test_support;
173//! use nu_test_support::prelude::*;
174//!
175//! #[test]
176//! fn pull_value_out() -> Result {
177//! #     unimplemented!()
178//! # }
179//! #
180//! # fn main() -> Result {
181//!     let num: f64 = test().run("12.34 + 2")?;
182//!     assert_eq!(num.floor(), 14.0);
183//!     Ok(())
184//! }
185//! ```
186//!
187//! ## Running Multiple Snippets on a Single Tester
188//!
189//! Some tests require executing multiple snippets instead of a single pipeline.
190//! Running them sequentially can also improve readability, especially for commands that return
191//! [`Nothing`](nu_protocol::Value::Nothing).
192//!
193//! A single tester instance can be reused to execute multiple snippets in order, allowing state to
194//! be built up step by step:
195//!
196//! ```
197//! # #[macro_use] extern crate nu_test_support;
198//! use nu_test_support::prelude::*;
199//!
200//! #[test]
201//! fn multiple_statements() -> Result {
202//! #     unimplemented!()
203//! # }
204//! #
205//! # fn main() -> Result {
206//!     let mut tester = test();
207//!     let () = tester.run("def parrot [] { '🦜' }")?;
208//!     let () = tester.run("def duck [] { '🦆' }")?;
209//!     tester
210//!         .run("(parrot) + 🤝 + (duck)")
211//!         .expect_value_eq("🦜🤝🦆")
212//! }
213//! ```
214//!
215//! ## Inserting Data
216//!
217//! In some cases, it is more convenient to pass data into a pipeline directly
218//! instead of constructing it in Nushell code. The
219//! [`run_with_data`](tester::NuTester::run_with_data) method supports this by
220//! accepting a value that implements [`IntoValue`](nu_protocol::IntoValue).
221//!
222//! This is also useful to avoid using [`format!`], which can make tests harder
223//! to read or reason about.
224//!
225//! ```
226//! # #[macro_use] extern crate nu_test_support;
227//! use bytes::Bytes;
228//! use nu_test_support::prelude::*;
229//!
230//! #[test]
231//! fn decode_bytes() -> Result {
232//! #     unimplemented!()
233//! # }
234//! #
235//! # fn main() -> Result {
236//!     test()
237//!         .run_with_data("$in | decode", Bytes::from("hello world"))
238//!         .expect_value_eq("hello world")
239//! }
240//! ```
241//!
242//! Since both [`IntoValue`](nu_protocol::IntoValue) and [`FromValue`](nu_protocol::FromValue) can
243//! be derived, custom Rust types can be passed into Nushell and asserted directly.
244//! This keeps tests type safe and expressive.
245//!
246//! ```
247//! # #[macro_use] extern crate nu_test_support;
248//! use nu_test_support::prelude::*;
249//!
250//! #[derive(Debug, PartialEq, Eq, Clone, IntoValue, FromValue)]
251//! struct Sample {
252//!     a: String,
253//!     b: u32,
254//! }
255//!
256//! #[test]
257//! fn in_and_out() -> Result {
258//! #     unimplemented!()
259//! # }
260//! #
261//! # fn main() -> Result {
262//!     let sample = Sample {
263//!         a: "🐳".into(),
264//!         b: 52,
265//!     };
266//!
267//!     test()
268//!         .run_with_data("$in | to nuon | from nuon", sample.clone())
269//!         .expect_value_eq(sample)
270//! }
271//! ```
272//!
273//! ## Working with Metadata or Streams
274//!
275//! Some tests need access to metadata or streaming data.
276//! In these cases, [`run`](tester::NuTester::run) is not sufficient, since it returns a
277//! [`Value`](nu_protocol::Value).
278//!
279//! To work with lower level details, the raw [`PipelineData`](nu_protocol::PipelineData)
280//! can be obtained using [`run_raw`](tester::NuTester::run_raw) or
281//! [`run_raw_with_data`](tester::NuTester::run_raw_with_data).
282//!
283//! ```
284//! # #[macro_use] extern crate nu_test_support;
285//! use nu_test_support::prelude::*;
286//!
287//! #[test]
288//! fn check_metadata() -> Result {
289//! #     unimplemented!()
290//! # }
291//! #
292//! # fn main() -> Result {
293//!     let mut pipeline_data = test().run_raw("version | to nuon")?.body;
294//!     let metadata = pipeline_data.take_metadata().expect("should have metadata");
295//!     let content_type = metadata.content_type.expect("should have a content type");
296//!     assert_eq!(content_type, "application/x-nuon");
297//!     Ok(())
298//! }
299//! ```
300//!
301//! ## Configuring the Tester
302//!
303//! By default, the tester only includes Nushell builtins, the standard library,
304//! the `$nu` constant, and a minimal set of environment variables.
305//! For example, `$env.PATH` is unset to keep tests deterministic.
306//! When needed, the tester can be configured through a set of convenience methods.
307//!
308//! ### Setting the Working Directory
309//!
310//! The [`cwd`](tester::NuTester::cwd) method sets the current working directory (`$env.PWD`).
311//! This is useful when tests rely on filesystem access relative to a specific location.
312//!
313//! ```
314//! # #[macro_use] extern crate nu_test_support;
315//! use nu_test_support::prelude::*;
316//!
317//! #[test]
318//! fn cwd() -> Result {
319//! #     unimplemented!()
320//! # }
321//! #
322//! # fn main() -> Result {
323//!     test()
324//!         .cwd("./crates/nu-test-support")
325//!         .run("open Cargo.toml | get package.name")
326//!         .expect_value_eq("nu-test-support")
327//! }
328//! ```
329//!
330//! ### Configuring the Locale
331//!
332//! The [`locale`](tester::NuTester::locale) method overrides the locale, while
333//! [`locale_en`](tester::NuTester::locale_en) provides a convenient way to force English output.
334//! This is helpful when testing locale dependent behavior.
335//!
336//! ```
337//! # #[macro_use] extern crate nu_test_support;
338//! use nu_test_support::prelude::*;
339//!
340//! #[test]
341//! fn locale() -> Result {
342//! #     unimplemented!()
343//! # }
344//! #
345//! # fn main() -> Result {
346//!     let code = r#""2021-10-22 20:00:12 +01:00" | format date "%c""#;
347//!     let en: String = test().locale_en().run(&code)?;
348//!     let de: String = test().locale("de_DE").run(&code)?;
349//!     assert_ne!(en, de);
350//!     Ok(())
351//! }
352//! ```
353//!
354//! ### Inheriting the System PATH
355//!
356//! By default, external commands are not available since `$env.PATH` is unset.
357//! The [`inherit_path`](tester::NuTester::inherit_path) method restores access to the system PATH,
358//! allowing tests to call external binaries.
359//!
360//! ```
361//! # #[macro_use] extern crate nu_test_support;
362//! use nu_test_support::prelude::*;
363//!
364//! #[cfg(windows)]
365//! #[test]
366//! fn echo() -> Result {
367//! #     unimplemented!()
368//! # }
369//! # #[cfg(windows)]
370//! # fn main() -> Result {
371//!     test()
372//!         .inherit_path()
373//!         .run(r#"cmd.exe /c "echo abc""#)
374//!         .expect_value_eq("abc")
375//! }
376//!
377//! #[cfg(unix)]
378//! #[test]
379//! fn echo() -> Result {
380//! #     unimplemented!()
381//! # }
382//! # #[cfg(unix)]
383//! # fn main() -> Result {
384//!     test()
385//!         .inherit_path()
386//!         .run(r#"sh -c "echo abc""#)
387//!         .expect_value_eq("abc")
388//! }
389//! ```
390//!
391//! ### Using the Rust Toolchain
392//!
393//! The [`inherit_rust_toolchain_env`](tester::NuTester::inherit_rust_toolchain_env)
394//! method makes Rust tooling such as `cargo` or `rustc` available inside tests.
395//!
396//! ```
397//! # #[macro_use] extern crate nu_test_support;
398//! use nu_test_support::prelude::*;
399//!
400//! #[test]
401//! fn check_cargo_version() -> Result {
402//! #     unimplemented!()
403//! # }
404//! #
405//! # fn main() -> Result {
406//!     let code = r#"cargo --version | split row " " | get 0"#;
407//!     test()
408//!         .inherit_rust_toolchain_env()
409//!         .run(code)
410//!         .expect_value_eq("cargo")
411//! }
412//! ```
413//!
414//! ### Depending on Binaries and Plugins
415//!
416//! Tests that need a compiled Nushell binary should declare that requirement with the `#[deps]`
417//! attribute. For example, `#[deps(NU)]` ensures the `nu` binary is built and adds it to the
418//! tester's PATH, making `nu` available as an external command.
419//!
420//! ```no_run
421//! # #[macro_use] extern crate nu_test_support;
422//! use nu_test_support::prelude::*;
423//! use nu_test_support::value_types::CompleteResult;
424//!
425//! #[test]
426//! #[deps(NU)]
427//! fn cococo() -> Result {
428//! #     unimplemented!()
429//! # }
430//! #
431//! # fn main() -> Result {
432//!     let code = r#"nu -n -c 'print -e "cococo"; exit 1' | complete"#;
433//!     let result: CompleteResult = test().run(code)?;
434//!
435//!     assert_eq!(result.exit_code, 1);
436//!     assert_eq!(result.stderr, "cococo");
437//!     Ok(())
438//! }
439//! ```
440//!
441//! `#[deps]` also replaces plugin-specific setup. Plugin dependencies are preloaded into the
442//! tester, so plugin commands can be used directly without spawning a separate `nu` process.
443//!
444//! ```no_run
445//! # #[macro_use] extern crate nu_test_support;
446//! use nu_test_support::prelude::*;
447//!
448//! #[test]
449//! #[deps(NU_PLUGIN_EXAMPLE)]
450//! fn plugin_command() -> Result {
451//! #     unimplemented!()
452//! # }
453//! #
454//! # fn main() -> Result {
455//!     test().run("42 | example echo").expect_value_eq(42)
456//! }
457//! ```
458//!
459//! Multiple dependencies can be listed when a test needs more than one binary:
460//! `#[deps(NU, NU_PLUGIN_INC)]`.
461//! The dependency constants are exported by the [`prelude`] and from [`harness::deps`].
462//! Set the [`NU_TEST_SKIP_DEPS_BUILD`](harness::SKIP_DEPS_BUILD_ENV) environment variable to skip
463//! the build step when the expected binaries already exist in the target directory.
464//!
465//! ### Setting Environment Variables
466//!
467//! The [`env`](tester::NuTester::env) method sets environment variables for the tester itself.
468//! Unlike the `#[env]` attribute, this configures the tester instance directly rather than the
469//! test harness.
470//!
471//! ```
472//! # #[macro_use] extern crate nu_test_support;
473//! use nu_test_support::prelude::*;
474//!
475//! #[test]
476//! fn hey() -> Result {
477//! #     unimplemented!()
478//! # }
479//! #
480//! # fn main() -> Result {
481//!     test()
482//!         .env("HEY", "👋")
483//!         .run("$env.HEY")
484//!         .expect_value_eq("👋")
485//! }
486//! ```
487//!
488//! ## Using the Playground
489//!
490//! The [`Playground`](playground::Playground) provides a sandboxed filesystem
491//! environment for tests. This is especially useful when testing commands
492//! that modify the filesystem, such as creating or removing files.
493//!
494//! Tests typically combine the playground with [`cwd`](tester::NuTester::cwd)
495//! to point the tester to the sandboxed directory.
496//!
497//! ```
498//! # #[macro_use] extern crate nu_test_support;
499//! use nu_test_support::{fs::Stub::EmptyFile, prelude::*};
500//!
501//! #[test]
502//! fn rm_in_playground() -> Result {
503//! #     unimplemented!()
504//! # }
505//! #
506//! # fn main() -> Result {
507//!     Playground::setup("rm_in_doctest", |dirs, sandbox| {
508//!         sandbox.with_files(&[EmptyFile("i_will_be_deleted.txt")]);
509//!         test()
510//!             .cwd(dirs.test())
511//!             .run("rm i_will_be_deleted.txt")
512//!             .expect_value_eq(())
513//!     })
514//! }
515//! ```
516//!
517//! ## Configuring Experimental Options
518//!
519//! Experimental features can be enabled or disabled per test using the
520//! `#[exp]` attribute provided by the custom test harness.
521//!
522//! ```no_run
523//! # // this is a no_run as we cannot set experimental options safely during a doctest run
524//! # #[macro_use] extern crate nu_test_support;
525//! use nu_experimental::EXAMPLE;
526//! use nu_test_support::prelude::*;
527//!
528//! #[test]
529//! #[exp(EXAMPLE)]
530//! fn example_experimental_option() -> Result {
531//! #     unimplemented!()
532//! # }
533//! #
534//! # fn main() -> Result {
535//!     let code = "debug experimental-options | where identifier == example | get enabled.0";
536//!     test().run(code).expect_value_eq(true)
537//! }
538//! ```
539//!
540//! ## Using `rstest`
541//!
542//! The `rstest` crate provides support for fixtures and parameterized test cases, which can
543//! significantly reduce boilerplate.
544//! It is especially useful when testing the same logic with multiple inputs.
545//!
546//! It works out of the box with the custom test harness, but requires careful ordering when
547//! combined with additional test attributes.
548//!
549//! ```
550//! # #[macro_use] extern crate nu_test_support;
551//! use nu_test_support::prelude::*;
552//! use rstest::rstest;
553//!
554//! #[rstest]
555//! #[case("a", "a🦜a")]
556//! #[case("🦜", "🦜🦜🦜")]
557//! fn simple_case(#[case] pre_and_suffix: &str, #[case] result: &str) -> Result {
558//! #     unimplemented!()
559//! # }
560//! #
561//! # fn main() -> Result {
562//! # let pre_and_suffix = "a";
563//! # let result = "a🦜a";
564//!     test()
565//!         .run_with_data("$in + 🦜 + $in", pre_and_suffix)
566//!         .expect_value_eq(result)
567//! }
568//! ```
569//!
570//! When combining `rstest` with the custom test harness attributes, the order of attributes
571//! becomes important.
572//! The harness attribute must be explicitly specified to ensure the test is picked up correctly.
573//!
574//! ```
575//! # #[macro_use] extern crate nu_test_support;
576//! use nu_test_support::prelude::*;
577//! use rstest::rstest;
578//!
579//! #[rstest]
580//! #[case(1)]
581//! #[case(-1)]
582//! #[nu_test_support::test]
583//! #[env(QUICK_MATHS = "true")]
584//! fn math_abs(#[case] input: i32) -> Result {
585//! #     unimplemented!()
586//! # }
587//! #
588//! # fn main() -> Result {
589//! # let input: i32 = 1;
590//!     test()
591//!         .run_with_data("$in | math abs", input)
592//!         .expect_value_eq(1)
593//! }
594//! ```
595
596pub mod assertions;
597pub mod fs;
598pub mod harness;
599pub mod net;
600pub mod playground;
601pub mod value_types;
602
603pub mod deprecated;
604#[doc(no_inline)]
605pub use deprecated::*;
606
607pub mod tester;
608pub use tester::{Result, ShellErrorExt, TestError as Error, TestResultExt, test};
609
610/// Prelude for writing tests.
611pub mod prelude {
612    #[doc(no_inline)]
613    pub use super::{
614        assertions::*,
615        harness::deps::*,
616        playground::Playground,
617        tester::{Result, ShellErrorExt, TestError as Error, TestResultExt, WORKSPACE_ROOT, test},
618        value_types::*,
619    };
620
621    #[doc(no_inline)]
622    pub use nu_protocol::{
623        CompileError, FromValue, IntoValue, ParseError, ShellError, Value, test_list, test_record,
624        test_table, test_value,
625    };
626}
627
628// Expose macros to be used for the test harness.
629pub use harness::macros::*;
630
631// Needs to be reexported for `nu!` macro
632pub use nu_path;
633
634// Export json macro to allow writing json values easily.
635#[doc(no_inline)]
636pub use serde_json::json;
637
638/// Build a [`CellPath`](nu_protocol::ast::CellPath) in Rust using the familiar cell path syntax.
639///
640/// This macro lets you write cell paths the same way you do in Nushell.
641/// It also supports inline variables or expressions by wrapping them in a group (parentheses).
642///
643/// # Examples
644///
645/// ```rust
646/// use nu_test_support::test_cell_path;
647///
648/// let simple = test_cell_path!(foo.bar);
649/// assert_eq!(simple.to_string(), "$.foo.bar");
650///
651/// let with_modifiers = test_cell_path!(foo?.bar!);
652/// assert_eq!(with_modifiers.to_string(), "$.foo?.bar!");
653///
654/// let with_literal = test_cell_path!(foo."bar baz".3);
655/// assert_eq!(with_literal.to_string(), r#"$.foo."bar baz".3"#);
656///
657/// let column = "foo";
658/// let index = 3;
659/// let from_vars = test_cell_path!((column).(index));
660/// assert_eq!(from_vars.to_string(), "$.foo.3");
661/// ```
662#[doc(inline)]
663pub use nu_test_support_macros::test_cell_path;