Skip to main content

nu_test_support/
lib.rs

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