브라우저 확장 프로그램의 경쟁 조건 테스트 방법: AI 에이전트가 수정한 내용
요약
브라우저 확장 프로그램의 경쟁 조건(race condition) 테스트 방법을 다루며, 단순히 순차적인 호출로는 중첩 상황을 증명할 수 없음을 강조합니다. 불변성 정의, 재현 가능한 비동기 시나리오 구성, 그리고 탭별 잠금 및 외부 신호 처리를 통해 견고한 코드를 작성하는 것이 핵심입니다.
핵심 포인트
- 불변성을 정의하여 동작 시작과 완료 사이에 보존되어야 할 상태를 명확히 해야 합니다.
- Promise를 열어두는 등 비동기 호출을 제어하여 실제 중첩 상황을 재현해야 합니다.
- 단순한 뮤텍스 추가가 아닌, 어떤 액션이 상태를 공유하는지 정의하고 큐잉 로직을 설계해야 합니다.
- Mock 테스트 외에 탭 종료, DevTools 연결 실패 같은 Chrome 경계 시나리오를 반드시 포함해야 합니다.
간단 요약: 동작들이 겹칠 때 항상 참이어야 하는 불변성(invariant)을 정의하고, 테스트에서 그 완료 순서를 제어하며, 실제 브라우저 시나리오를 추가해야 합니다. 두 호출이 순차적으로 실행되는 테스트만으로는 코드가 중첩된 상황을 처리하는지 증명할 수 없습니다.
테스트 작성 전 불변성 정의하기
브라우저 확장 프로그램은 로컬 코드와 외부에서 변경되는 상태를 연결합니다. 탭이 닫히거나, 그룹이 바뀌거나, 디버깅 연결이 끊어질 수 있습니다. 따라서 동작의 시작과 완료 사이에 무엇이 보존되어야 하는지 질문해야 합니다.
real-browser-mcp에 대한 공개 기여에서, 동일한 탭에서 그룹 이름에 대한 두 요청이 두 개의 그룹을 생성할 수 있는 사례가 발생했습니다. PR #71은 탭별로 동작 순서를 지정하고 잠금(lock) 내부에서 탭 상태를 재읽는 수정 사항을 설명합니다. 이는 '동작 시작 시점에 읽은 상태가 동작이 시작될 때 오래된(stale) 상태일 수 있다'는 동일한 격차의 구체적인 예시입니다.
재현 가능한 중첩 상황 만들기
합성 예시: 두 요청이 그룹이 없는 탭을 확인합니다. 각각 하나를 생성할 준비를 합니다. 테스트에서는 첫 번째 호출의 Promise를 열어두고, 두 번째 호출을 시작한 다음, 비로소 첫 번째 호출을 완료시킵니다.
마지막에 그룹 생성이 몇 번 실행되었는지와 탭의 최종 상태가 어떤지 확인합니다. 요구 사항이 그룹 하나라면, '성공' 결과가 두 개라는 것만으로는 충분하지 않습니다. 에이전트에게 동일한 동작 순서에서 이전 버전은 실패하고 새 버전은 요구 사항을 충족함을 보여달라고 요청해야 합니다.
예시는 테스트를 위한 제안 방식입니다. 완료 순서는 코드를 위해 편리한 대체재가 아니라 실제 인터페이스를 나타내야 합니다.
올바른 리소스에 잠금(Lock) 걸기
탭별 잠금은 탭의 그룹 동작에 적용될 수 있습니다. 확장 프로그램 전체에서 공유되는 리소스는 다른 사고방식이 필요합니다. PR #72는 디버거 연결을 획득하고 사용하는 단일 큐를 설명하며, 부분적인 추적과 함께 두 개의 중첩된 획득을 방지합니다.
선택은 단순히 '뮤텍스(mutex)'를 추가하는 것이 아닙니다. 어떤 액션들이 상태(state)를 공유하는지, 무엇이 병렬로 실행될 수 있는지, 그리고 가드(guard)가 언제 해제되는지를 정의해야 합니다. 또한 액션 내부의 오류도 테스트해야 합니다. 실패하더라도 다음 액션이 계약(contract)에 따라 진행할 수 있는 방법은 남겨두어야 하며, 큐를 막아버려서는 안 됩니다.
외부 신호와 연결 소유권 분리하기
Chrome 문서에 따르면, 디버거 연결 실패는 promise를 거부합니다. onDetach 이벤트는 브라우저가 세션을 종료할 때 도착하는데, 예를 들어 탭이 닫히거나 DevTools가 연결된 탭에서 열릴 때입니다.
다른 디버거에 대한 메시지가 당신의 확장 프로그램이 연결을 가지고 있다는 증거는 아닙니다. PR #72에서는 작업자(worker) 재시작 후에도 확장 프로그램이 여전히 보유하고 있는 연결과 연결 실패를 구별하려는 프로브(probe)를 설명합니다. 코드가 실제로 구현하는 동작에 대해 두 경로 모두 테스트해야 합니다.
Chrome 경계에서 테스트 추가하기
Mock을 사용한 단위 테스트는 중첩(overlap)을 제어하고 호출 횟수를 셀 수 있게 해줍니다. 하지만 권한(permissions), 브라우저 메시지, 그리고 종료 이벤트가 예상대로 동작하는지 증명하지는 못합니다.
제안된 시나리오에 따라 두 개의 중첩 요청, 액션 도중 탭이 닫히는 경우, 세션 중에 DevTools가 열리는 경우를 테스트해야 합니다. 디버거를 사용하는 확장 프로그램은 매니페스트(manifest)에서 디버거 권한을 필요로 하고, tabGroups API를 사용하려면 tabGroups 권한이 필요합니다. 권한을 실제 구현에 맞추고, 단순히 테스트 통과를 위해 권한을 추가해서는 안 됩니다.
계층을 분리하는 보고서 요청하기
작업이 완료되면 에이전트는 Mock으로 무엇을 테스트했는지, 브라우저에서 무엇을 테스트했는지, 이벤트 순서는 어떠했는지, 그리고 어떤 버전 기준으로 테스트를 실행했는지를 명시해야 합니다. 모든 가능한 경쟁(race)에 대한 커버리지(coverage)를 약속할 필요는 없습니다. 필요한 것은 발견된 시나리오에 대한 증거와 아직 테스트가 필요한 부분에 대한 명확한 경계입니다.
제 경험을 바탕으로
실제 브라우저 MCP에 기여하는 것에 대한 포스팅에서, 저는 검토 후 두 개의 별도 PR(Pull Request)과 테스트 과정에서 발견된 케이스에 대한 수정 사항을 가지고 돌아온 한 기여자(contributor)를 설명했습니다. 이 가이드는 모든 변경 사항이 이미 테스트되었다고 가정하지 않고도 다른 기여에 적용할 수 있는 작동 방식을 제안합니다.
요약
액션들이 중첩될 때 항상 참이어야 하는 것을 정의하고, 테스트에서 그 완료 순서를 제어하며, 실제 브라우저 시나리오를 추가해야 합니다. 두 호출이 하나씩 실행되는 테스트는 코드가 중첩을 처리하는지 증명하지 못합니다.
FAQ
Promise.all만으로 레이스(race)를 테스트하기에 충분한가요?
중첩된 호출을 시작할 수는 있지만, 문제가 되는 순서가 실제로 발생했음을 보장하지는 않습니다. await 지점을 제어하고 최종 상태를 확인해야 합니다.
확장 프로그램의 모든 액션이 잠겨야 하나요?
공유 상태와 요구 사항에 따라 가드(guard)를 선택해야 합니다. 너무 광범위한 락(lock)은 허용된 병렬 처리를 차단할 수 있고, 너무 좁은 락은 충돌을 남길 수 있습니다.
목업 테스트가 브라우저 테스트를 대체할 수 있나요?
이벤트 순서를 제어하는 데 도움이 됩니다. 권한(Permissions), 분리(detachments), 그리고 Chrome의 동작 또한 실제 경계에서 테스트해야 합니다.
저자 소개
Ofer Shapira - AI 빌더이자 개발팀 리드로서, 에이전트, 결정, 그리고 검증 가능한 워크플로우에 대해 글을 쓰고 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기