브라우저 확장/단위 테스트(2)


요약

이전 글에서 다뤘던 아이디어를 실제 구현하여, 실제 브라우저 확장 개발과정에 적용하기까지의 과정을 소개한다.

결과물

Anymine이라고 이름붙인 이 패키지는 npm run test 등으로 실행하는 것을 가정하고 만들었다. 실행시 watch 모드로 동작하며, 주어진 테스트 코드와 그 dependency를 지켜보면서 변경이 있을 때마다 컴파일해서 테스트를 실행, 그 결과를 보여주는 것을 목표로 한다.

알고봤더니 headless 크롬에서 브라우저 확장을 로드할 수 있게 된 것 자체가 최근의 일이었다. Chrome 96부터 등장한 --headless=chrome 옵션을 통해서나 headless 크롬에서 브라우저 확장을 로드할 수 있게 되었고, 이게 새로 브랜딩 된 것이 --headless=new 옵션인데 109 버전에 적용됐고 2023년 2월 블로그에 소개가 되었다. 그렇다 보니 상대적으로 이런 기능을 활용해서 테스트 툴을 만드는 시도가 없었던 것 아닌가 싶다.

구조

다음 패키지로 구성되어있다.

  • anymine-interface: 실행환경이 구현해야 하는 인터페이스를 정의한 패키지. Runtime이라는 인터페이스 하나만 정의하고 있다.
  • anymine-vm-runtime: Runtime 구현체 중 Node VM을 이용해서 구현한 구현체, 다른 구현체에 비해 간단한 구현이기 때문에 작은 테스트를 돌리기 좋다.
  • anymine-webext-runtime: Headless browser의 background script를 대상으로 하는 Runtime 구현체. 실제 브라우저 확장에서 사용하는 chrome 인스턴스를 바로 제공하기 때문에 원하는 기능 테스트가 용이하다.
  • anymine-packer: 테스트 대상 파일들을 지켜보면서 JS코드로 컴파일한 결과를 AsyncIterableIterator로 제공하는 패키지.
  • anymine: 테스트 환경 구축 패키지. Runtime에게 Jasmine 테스트 환경을 삽입하고, 테스트 JS 코드를 실행한 다음, 테스트 결과 표시, 테스트 환경 재설정 등의 테스트 lifecycle을 구현하고 있다.

사용방법

현재는 유저가 직접 JS코드를 작성해서 실행할 것을 가정하고 있다. 대충 이런 식이다.

import { test } from "anymine";

(async () => {
  for await (const specs of test({
    glob: "./src/background/**/*.{spec,test}.ts",
    watch: true,
    runtime: "webext",
  })) {
    let allPass = true;
    for (const spec of specs) {
      if (spec.status !== "failed") {
        continue;
      }
      allPass = false;
      console.log(spec.failedExpectations[0]);
    }
    if (allPass) {
      console.log("All tests passed");
    }
  }
})();

현재는 테스트 실패한 것만 바로 보면서 고치는 것에 집중하고 있어서 간단하게 구현했는데, 추후 시간이 되면 전체 테스트 결과를 보관하고, 파일 변경에 따라 추가로 테스트 결과가 바뀌는 것만 적용하는 식으로 전체 테스트 실행 상황을 일목요연하게 보여주는 UI를 구현하고 싶다.

그런데

그동안 외부 의존성 0으로 브라우저 확장을 작성하고 있어서 몰랐는데, 외부 의존을 제대로 처리하지 못하고 있었다! 아마도 빌드 시스템에 해당하는 packer에서 의존성을 같이 버무려 주면 될 것 같은데 이런 간단한 것을 못하는 채로 쓰고 있었다니;; 역시 이래서 인하우스 툴은 안되는 것인가.. 가뜩이나 구현할 기능은 많고 테스트도 이것저것 터지기 시작했는데 테스트 시스템에 더 시간투자를 할 여력이 애매한데 그냥 테스트 없이 콘솔에 찍어가면서 개발해야 하나 골치가 아파온다…