How to Capture rlang::informMessages with sink() in R Unit Tests

Summary

A minimal yet realistic failure: rlang::inform() messages are not captured by a simple sink() call because they are sent to the message output stream, not the standard output stream. By redirecting both streams—type = "output" for print()/cat() and type = "message" for rlang::inform()/warning()/abort()—unit tests and logging frameworks can capture the full user‑visible output.

Root Cause

  • rlang::inform() (and the family of rlang messaging functions) write to the message connection (message()/cat(..., file = stderr()) internally).
  • sink() without a type argument redirects standard output (stdout).
  • Therefore, a plain sink() only captures print(), cat(), etc., while messages emitted by rlang bypass it.
  • The error “file must be NULL or an already open connection” appears when attempting sink(file="Sunk.txt", type="message") because sink() expects a file name or a pre‑opened connection; passing a string directly for the message stream is disallowed.

Why This Happens in Real Systems

  • Separation of concerns: stdout is for regular program output; stderr (message stream) is for diagnostics, warnings, and informative prompts.
  • Interactivity and tooling: IDEs and REPLs capture stderr separately to provide colored warnings or interrupt controls.
  • Testing hygiene: Keeping diagnostics on stderr prevents accidental consumption by consumers that parse stdout.

Real-World Impact

  • Unit tests that rely on sink() to capture all output will miss useful information from rlang::inform().
  • Debugging prints may be silently discarded, leading to confusion when diagnostics appear on the console but not in log files.
  • CI pipelines that redirect logs may contain incomplete traces, making reproducibility harder.

Example or Code (if necessary and relevant)

library(rlang)

HelloWorld <- function() {
  rlang::inform(message = "Hello, World!")
}

# Correct capture of both output and message streams
fout <- file("Sunk.txt", open = "wt")

# Redirect standard output (stdout)
sink(fout, type = "output")

# Redirect message stream (stderr)
sink(fout, type = "message")

HelloWorld()

# Stop redirecting
sink(type = "output")
sink(type = "message")

close(fout)

The file Sunk.txt now contains:

Hello, World!

How Senior Engineers Fix It

  1. Explicitly redirect both streams in test harnesses or logging wrappers.
  2. Use helper functions to encapsulate the pattern:
    redirect_all <- function(fname) {
    f <- file(fname, open = "wt")
    sink(f, type = "output")
    sink(f, type = "message")
    invisible(f)
    }

close_all <- function(f) {
sink(type = “output”)
sink(type = “message”)
close(f)
}

3. For production logging, prefer a dedicated logging package (e.g., `crayon`, `logging`) that handles both streams gracefully.  
4. When writing reusable utilities, document the need to capture the *message* stream.

## Why Juniors Miss It  

- **Assumption of universal capture**: They believe a single `sink()` suffices for all user‑visible output.  
- **Ignorance of R’s I/O streams**: Many new developers are unaware that R separates stdout and stderr internally.  
- **Overreliance on ad‑hoc examples**: Encountering `print()`/`cat()` examples, they ignore that `rlang` functions solve a different problem (muffling, warnings).  
- **Limited error handling**: The cryptic sink error message may discourage deeper investigation.

By clarifying the distinction between output streams and providing a concise pattern to capture both, senior engineers prevent silent loss of diagnostic information in real systems.

Leave a Comment