(integration-testing-vms)=

# NixOS 가상 머신을 이용한 통합 테스트

```{contributors}
:authors: olafklingt, domenkozar
:editors: fricklerhandwerk
```

## 무엇을 배우게 됩니까?

이 튜토리얼에서는 NixOS 설정을 테스트하기 위한 Nixpkgs 기능을 소개합니다. 또한 여러 머신이 관여하는 분산 테스트 시나리오를 구성하는 방법도 보여 줍니다.

## 무엇이 필요합니까?

- Linux에서 동작하는 [Nix 설치](<install-nix>) 또는 [NixOS](https://nixos.org/manual/nixos/stable/index.html#sec-installation)
- [Nix 언어](<reading-nix-language>)에 대한 기본 지식
- [NixOS 설정](<nixos-vms>)에 대한 기본 지식

## 소개

Nixpkgs는 분산 시스템의 통합 테스트를 자동화하는 [테스트 환경](https://nixos.org/manual/nixos/stable/index.html#sec-nixos-tests)을 제공합니다. 이 환경을 사용하면 선언적 NixOS 설정의 집합을 바탕으로 테스트를 정의하고, [QEMU](https://www.qemu.org/)를 백엔드로 삼아 Python 셸로 해당 설정들과 상호 작용할 수 있습니다. 이러한 테스트는 NixOS가 의도대로 동작하는지 확인하는 데 널리 사용되므로, 일반적으로 [NixOS 테스트](https://nixos.org/manual/nixos/stable/index.html#sec-nixos-tests)라고 부릅니다. 이 테스트는 NixOS 외부의 어떤 Linux 머신[^darwin]에서도 작성하고 실행할 수 있습니다.

[^darwin]: [macOS에서 NixOS VM 테스트를 실행하는 기능](https://github.com/NixOS/nixpkgs/issues/108984)도 구현되어 있지만 [현재 문서화되어 있지 않습니다](https://github.com/NixOS/nixpkgs/issues/254552).

Nix의 설계 특성 덕분에 통합 테스트는 재현 가능하며, 이 때문에 지속적 통합(CI) 파이프라인에서 유용합니다.

## `testers.runNixOSTest` 함수

NixOS VM 테스트는 `testers.runNixOSTest` 함수를 사용하여 정의합니다. NixOS VM 테스트의 패턴은 다음과 같습니다.

```nix
let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.11";
  pkgs = import nixpkgs { config = {}; overlays = []; };
in

pkgs.testers.runNixOSTest {
  name = "test-name";
  nodes = {
    machine1 = { config, pkgs, ... }: {
      # ...
    };
    machine2 = { config, pkgs, ... }: {
      # ...
    };
  };
  testScript = { nodes, ... }: ''
    # ...
  '';
}
```

`testers.runNixOSTest` 함수는 [테스트 옵션](https://nixos.org/manual/nixos/stable/index.html#sec-test-options-reference)을 지정하는 [모듈](https://nixos.org/manual/nixos/stable/#sec-writing-modules)을 인자로 받습니다. 이 모듈은 설정 값만 지정하므로 축약된 모듈 표기법을 사용할 수 있습니다.

다음 설정 값을 반드시 지정해야 합니다.

- [`name`](https://nixos.org/manual/nixos/stable/index.html#test-opt-name)은 테스트의 이름을 정의합니다.

- 테스트 스크립트에는 가상 머신이 둘 이상 관여할 수 있으므로, [`nodes`](https://nixos.org/manual/nixos/stable/index.html#test-opt-nodes)에는 이름이 붙은 설정의 집합이 들어 있습니다. 각 가상 머신은 NixOS 설정으로부터 생성됩니다.

- [`testScript`](https://nixos.org/manual/nixos/stable/index.html#test-opt-testScript)는 Python 테스트 스크립트를 리터럴 문자열 또는 `nodes` 속성을 받는 함수 형태로 정의합니다. 이 Python 테스트 스크립트는 `nodes`에 사용된 이름을 통해 가상 머신에 접근할 수 있습니다. 이 스크립트는 가상 머신에서 슈퍼유저 권한을 가집니다. Python 스크립트에서 각 가상 머신은 `machine` 객체를 통해 접근할 수 있습니다. NixOS는 이러한 설정에 대해 테스트를 실행하는 [메서드](https://nixos.org/manual/nixos/stable/index.html#ssec-machine-objects)를 제공합니다.

테스트 프레임워크는 가상 머신을 자동으로 시작하고 Python 스크립트를 실행합니다.

## 최소 예제

기본 설정에 대한 최소한의 테스트로, 사용자 `root`와 `alice`가 Firefox를 실행할 수 있는지 확인하겠습니다. 예제는 처음부터 직접 만들어 나가겠습니다.

1. [고정된 버전의 Nixpkgs](ref-pinning-nixpkgs)를 사용하고, 전역 설정에 의해 의도치 않게 오버라이드되지 않도록 [설정 옵션과 오버레이를 명시적으로 지정](nixpkgs-config)하십시오:

   ```nix
   let
     nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.11";
     pkgs = import nixpkgs { config = {}; overlays = []; };
   in

   pkgs.testers.runNixOSTest {
     # ...
   }
   ```

1. 테스트에 내용을 잘 설명하는 이름을 붙이십시오:

   ```nix
   name = "minimal-test";
   ```

1. 이 예제에서는 가상 머신을 하나만 사용하므로, 지정하는 노드의 이름은 간단히 `machine`입니다. 이 이름은 임의로 정한 것이며 자유롭게 선택할 수 있습니다. 설정으로는 [이전 튜토리얼에서 사용했던](<nixos-vms>) 기본 설정 중 관련된 부분을 사용합니다:

   ```nix
   nodes.machine = { config, pkgs, ... }: {
     users.users.alice = {
       isNormalUser = true;
       extraGroups = [ "wheel" ];
       packages = with pkgs; [
         firefox
         tree
       ];
     };

     system.stateVersion = "23.11";
   };
   ```

1. 테스트 스크립트는 다음과 같습니다:

   ```python
   machine.wait_for_unit("default.target")
   machine.succeed("su -- alice -c 'which firefox'")
   machine.fail("su -- root -c 'which firefox'")
   ```

   이 Python 스크립트는 `nodes` 속성 집합에서 사용한 가상 머신 설정에 붙인 이름인 `machine`을 참조합니다.

   이 스크립트는 systemd가 `default.target`에 도달할 때까지 기다립니다. `su` 명령으로 사용자를 전환하고, `which` 명령으로 해당 사용자가 `firefox`에 접근할 수 있는지 확인합니다. `which firefox` 명령이 사용자 `alice`에게는 성공하고 `root`에게는 실패할 것으로 기대합니다.

   이 스크립트는 `testScript` 속성의 값이 됩니다.

완성된 `minimal-test.nix` 파일의 전체 내용은 다음과 같습니다:

```nix
let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.11";
  pkgs = import nixpkgs { config = {}; overlays = []; };
in

pkgs.testers.runNixOSTest {
  name = "minimal-test";

  nodes.machine = { config, pkgs, ... }: {

    users.users.alice = {
      isNormalUser = true;
      extraGroups = [ "wheel" ];
      packages = with pkgs; [
        firefox
        tree
      ];
    };

    system.stateVersion = "23.11";
  };

  testScript = ''
    machine.wait_for_unit("default.target")
    machine.succeed("su -- alice -c 'which firefox'")
    machine.fail("su -- root -c 'which firefox'")
  '';
}
```

## 테스트 실행하기

모든 머신을 준비하고 테스트 스크립트를 실행하려면 다음과 같이 하십시오:

```shell-session
$ nix-build minimal-test.nix
```

    ...
    test script finished in 10.96s
    cleaning up
    killing machine (pid 10)
    (0.00 seconds)
    /nix/store/bx7z3imvxxpwkkza10vb23czhw7873w2-vm-test-run-minimal-test

## 가상 머신의 대화형 Python 셸

테스트를 개발하거나 무언가가 잘못되었을 때는 테스트를 대화형으로 조작해 보거나 머신의 터미널에 접속하는 것이 유용합니다.

테스트 프레임워크로 대화형 Python 세션을 시작하려면 다음과 같이 하십시오:

```shell-session
$ $(nix-build -A driverInteractive minimal-test.nix)/bin/nixos-test-driver
```

여기에서는 어떤 테스트 작업이든 실행할 수 있습니다. `test_script()` 함수로 `minimal-test.nix`의 `testScript` 속성을 실행하십시오.

가상 머신이 아직 시작되지 않았다면, `machine` 객체의 메서드를 처음 호출할 때 테스트 환경이 가상 머신을 시작합니다.

하지만 다음 명령으로 가상 머신을 직접 시작할 수도 있습니다:

```shell-session
>>> machine.start()
```
특정 노드의 경우,

또는

```shell-session
>>> start_all()
```
모든 노드의 경우입니다.

다음 명령으로 가상 머신의 대화형 셸에 들어갈 수 있습니다:

```shell-session
>>> machine.shell_interact()
```

그리고 다음과 같은 셸 명령을 실행할 수 있습니다:

```shell-session
uname -a
```

    Linux server 5.10.37 #1-NixOS SMP Fri May 14 07:50:46 UTC 2021 x86_64 GNU/Linux


::::{dropdown} 성공한 테스트 다시 실행하기

<!-- FIXME: this should be a separate recipe that can be linked to, as it's a bit of knowledge one will need now and again. -->

테스트 결과는 Nix 스토어에 보관되므로, 성공한 테스트는 캐시됩니다. 즉, 테스트 설정(노드 설정과 테스트 스크립트)이 의미상 동일하게 유지되는 한 Nix는 테스트를 다시 실행하지 않습니다. 따라서 테스트를 다시 실행하려면 결과를 삭제해야 합니다.

심볼릭 링크를 통해 결과를 삭제하려고 하면 다음과 같은 오류가 발생합니다:

```shell-session
nix-store --delete ./result
```

    finding garbage collector roots...
    0 store paths deleted, 0.00 MiB freed
    error: Cannot delete path '/nix/store/4klj06bsilkqkn6h2sia8dcsi72wbcfl-vm-test-run-unnamed' since it is still alive. To find out why, use: nix-store --query --roots

대신 심볼릭 링크를 먼저 제거한 다음에 캐시된 결과를 제거하십시오:

```shell-session
rm ./result
nix-store --delete /nix/store/4klj06bsilkqkn6h2sia8dcsi72wbcfl-vm-test-run-unnamed
```

이 작업은 명령 하나로도 수행할 수 있습니다:

```shell-session
result=$(readlink -f ./result) rm ./result && nix-store --delete $result
```
::::

## 여러 가상 머신을 사용하는 테스트

테스트에는 여러 가상 머신이 포함될 수 있습니다. 예를 들어 클라이언트-서버 간 통신을 테스트할 수 있습니다.

다음 예제 구성에는 다음이 포함됩니다:
- 기본 설정으로 [nginx](https://nginx.org/en/)를 실행하는 `server`라는 이름의 가상 머신.
- HTTP 요청을 보낼 수 있도록 `curl`을 사용할 수 있는 `client`라는 이름의 가상 머신.
- `client`와 `server` 사이의 테스트 로직을 조율하는 `testScript`.

`client-server-test.nix` 파일의 전체 내용은 다음과 같습니다:

```{code-block}
let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.11";
  pkgs = import nixpkgs { config = {}; overlays = []; };
in

pkgs.testers.runNixOSTest {
  name = "client-server-test";

  nodes.server = { pkgs, ... }: {
    networking = {
      firewall = {
        allowedTCPPorts = [ 80 ];
      };
    };
    services.nginx = {
      enable = true;
      virtualHosts."server" = {};
    };
  };

  nodes.client = { pkgs, ... }: {
    environment.systemPackages = with pkgs; [
      curl
    ];
  };

  testScript = ''
    server.wait_for_unit("default.target")
    client.wait_for_unit("default.target")
    client.succeed("curl http://server/ | grep -o \"Welcome to nginx!\"")
  '';
}
```

테스트 스크립트는 다음 단계를 수행합니다:
1) 서버를 시작하고 준비될 때까지 기다립니다.
1) 클라이언트를 시작하고 준비될 때까지 기다립니다.
1) 클라이언트에서 `curl`을 실행하고 `grep`을 사용하여 예상되는 반환 문자열을 확인합니다. 테스트는 반환 값에 따라 통과하거나 실패합니다.

테스트를 실행하십시오:

```shell-session
$ nix-build client-server-test.nix
```

## NixOS 테스트에 관한 추가 정보

- CI에서 통합 테스트를 실행하려면 하드웨어 가속이 필요하지만, 많은 CI가 이를 지원하지 않습니다.

  [GitHub Actions](<github-actions>)에서 통합 테스트를 실행하려면 [하드웨어 가속을 비활성화하는 방법](https://github.com/cachix/install-nix-action#how-do-i-run-nixos-tests)을 참고하십시오.

- NixOS에는 교육용 예제로 활용할 수 있는 방대한 테스트 모음이 포함되어 있습니다.

  [IRC와의 Matrix 브리징](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/matrix/appservice-irc.nix)이 좋은 참고 사례입니다.

<!-- TODO: move examples from https://wiki.nixos.org/wiki/NixOS_Testing_library to the NixOS manual and troubleshooting tips to nix.dev -->

## 다음 단계

- [](module-system-deep-dive)
- [](bootable-iso-image)
- [](nixos-docker-images)
