native_sim is not a unit test. Zephyr had to write that down.

Both targets produce a Linux executable. Both sit under Twister. Both let you debug
with gdb. So people treat native_sim and BOARD=unit_testing as two spellings of
"run Zephyr on the host."
Zephyr's own test-framework page opens a subsection by admitting the two are easy to confuse, then says: they are fundamentally different.1
When a project has to write that sentence, the confusion is not theoretical. It is the default.
Two host binaries, two machines
unit_testing is a pseudo-board. arch: unit. It is not hardware and it is not a
simulated SoC. Twister selects it when the scenario sets type: unit. The build
links the files you put on the testbinary target with the Ztest harness, using the
host toolchain. The kernel is not in the image. There is no boot, no scheduler, no
devicetree init, no driver model. You call the function. If it needed a kernel API,
you stub it.1
native_sim builds the complete Zephyr OS — kernel, devicetree, Kconfig, drivers,
subsystems — into a host binary that boots and runs like a Zephyr image, compiled
for the host instead of a target SoC. Tests that use it do not set type: unit.12
Reach for native_sim to exercise code
in a running Zephyr system. Reach for unit_testing to test an isolated module
without pulling in the kernel.1

What native_sim is honest about
The board page is blunt. native_sim "does not intend to simulate any particular
HW." It offers a few peripherals — Ethernet, display, UART — so application code
that needs those surfaces can run. It does not model a chip. Code is compiled for
the host, typically x86. There is no I/O or MMU emulation; a hardcoded address
segfaults.3 Time is simulated and, by default, decoupled from wall time. The kernel
thinks time is whatever the models say.2
The POSIX-architecture writeup underneath it is even blunter. The port assumes code executes in zero simulated time. It is not for debugging hardware/software races or missed programming deadlines. It does not replace an instruction-set simulator, a development board, or QEMU. It complements them.3
That last sentence is the one CI setups skip. They see a green Twister run on
native_sim and file it under "we tested on a board." They tested on a host
program that contains a Zephyr kernel. That is a real test. It is not a board.
native_posix is the old name. Same idea.2
Why the mix-up keeps happening
Twister's console output reports how a test ran: qemu, native_sim, or build-only.
The status line does not say whether the kernel was in the binary.4 If you
live in the YAML and not in the CMake, type: unit is a small key. Miss it and
you built a different kind of program than you think.
The 2018 argument is still the argument. One engineer wrote that ztest is for unit tests and native_posix is for system tests, and that you cannot mock the world away under native_posix because the world is the point. Another wanted native_posix to replace the mock layer so they would not have to maintain empty headers. The thread did not resolve into one tool. It resolved into two jobs.5
Zephyr later grew FFF mocks inside Ztest, which makes the unit-testing board more
pleasant and does not make native_sim a unit-test target. Different layer.
I have made this split before. Firmware CI that ends at the linker is the unit-testing failure mode: you compiled something, you did not run the system. Simulation versus HIL is the native_sim failure mode: you ran a system, it was not the silicon. Both belong in CI. They answer different tickets.

What you can claim after a native_sim pass
You can claim the kernel scheduled, the app reached the state you asserted, the subsystem you enabled initialized, and a host-backed UART or Ethernet path did what that backend does. You can debug it with host tools, ASan, UBSan, coverage. Those are large claims. They are why the target exists.2
You cannot claim the STM32 timer did the thing, the nRF radio did the thing, or
the image you will flash did the thing. That image was not built. The ABI is the
host ABI — 32-bit native_sim exists specifically because most MCUs are ILP32
and 64-bit host longs hide bugs.2 Even the 32-bit target is still host code
running host instructions.
If the behavior you care about is "does this module return the right value given
these stubs," use unit_testing. If it is "does this Zephyr app boot and pass
its integration tests without a board," use native_sim. If it is "does this
driver speak this chip," you have left both pages. That is what simulation
can't catch when people pretend otherwise.
The docs already picked the words. Fundamentally different. Use them.
Sources
Footnotes
-
Zephyr Project, "Test Framework," Zephyr documentation, accessed 2026-08-14. https://docs.zephyrproject.org/latest/develop/test/ztest.html ↩ ↩2 ↩3 ↩4
-
Zephyr Project, "Native simulator - native_sim," Zephyr documentation, accessed 2026-08-14. https://docs.zephyrproject.org/latest/boards/native/native_sim/doc/index.html ↩ ↩2 ↩3 ↩4 ↩5
-
Zephyr Project, "The POSIX architecture," Zephyr documentation, accessed 2026-08-14. https://docs.zephyrproject.org/latest/boards/native/doc/arch_soc.html ↩ ↩2
-
Zephyr Project, "Test Runner (Twister)," Zephyr documentation, accessed 2026-08-14. https://docs.zephyrproject.org/latest/develop/twister/index.html ↩
-
Zephyr Project, "native_posix supersedes ztest mocking," GitHub issue 7316, 2018-05-02. https://github.com/zephyrproject-rtos/zephyr/issues/7316 ↩