v13.39.0의 구현을 확인한 내용입니다. 기본 조작은 HTTP 클라이언트를, 검증 환경 준비는 패키지 테스트를 참조하세요.
Factory는 공유하고 PendingRequest는 매번 만든다
Laravel의 HTTP 클라이언트에는 서로 다른 책임을 가진 객체가 있습니다.FoundationServiceProvider는 Factory를 singleton으로 등록하고, Http 파사드도 같은 클래스 이름을 사용해 컨테이너에서 해석합니다. 따라서 일반적인 Laravel 애플리케이션에서는 컨테이너에서 받은 Factory를 통한 통신도 Http::fake()로 검증할 수 있습니다. 파사드를 직접 사용하는 설계도 가능하지만, 이 페이지에서는 의존 관계를 명시하기 위해 Factory를 주입합니다.
Factory에 대한 baseUrl() 등 일반적인 요청 설정 메서드는 __call()을 통해 새로운 PendingRequest로 전달됩니다. createPendingRequest()는 생성 시점의 fake용 콜백과 stray request 제한을 넘기고, newPendingRequest()는 공통 미들웨어와 공통 옵션을 적용합니다.
API 조작을 작은 클라이언트로 정리하기
가상의 카탈로그 API에서 상품을 가져오는Acme\Catalog 패키지를 예로 듭니다. src/CatalogClient.php는 Factory와 베이스 URL만 보관하고, 요청은 product() 실행 중에 만듭니다.
product(42, 'ja') 다음에 product(42)를 호출해도, 이전 호출에서 추가한 Accept-Language는 남지 않습니다. 사용하는 쪽 애플리케이션이 Factory에 설정한 공통 설정은 계속 각 요청에 적용됩니다.
- 패키지 고유의 설정은 새로 만든 PendingRequest에 적용합니다. 패키지 초기화에서
Http::globalOptions()를 설정하면 관계없는 통신에도 영향을 주므로 피합니다. - 생성자에서는 전송하지 않습니다. 서비스 해석, Artisan 실행, 설정 파일 공개만으로 외부 API에 접속하지 않는 설계로 합니다.
throw()는 4xx·5xx를RequestException으로 만듭니다. 접속 실패나 타임아웃의ConnectionException과 구별하고, 실패를 빈 배열 등의 성공 값으로 바꾸지 않습니다.json(flags: JSON_THROW_ON_ERROR)는 잘못된 JSON을JsonException으로 만듭니다. 예제에서는 배열인지 여부만 검사하므로, 실제 API에서는 필수 필드나 데이터 타입도 검증합니다.
서비스 프로바이더에서 등록하기
패키지의config/catalog.php에 초기 설정을 준비합니다. 여기에 HTTP 클라이언트나 클로저를 저장하지 말고, 설정 캐시에 저장할 수 있는 값만 둡니다.
src/CatalogServiceProvider.php는 설정을 병합하고, 클라이언트를 해석할 때 공유 Factory를 넘깁니다. Factory 자체의 바인딩은 덮어쓰지 않습니다.
config/acme-catalog.php의 베이스 URL을 실제 API로 변경합니다. 예제의 catalog.example.test는 설명과 fake용 가상 호스트입니다.
bind()를 사용하지만, 클라이언트를 보관하는 쪽이 singleton이라면 그 참조까지 매번 새로 만들어지는 것은 아닙니다. 호출 고유의 언어, 인증 정보, 요청 객체를 프로퍼티에 저장하지 않는 것이 중요합니다. 자세한 내용은 패키지 서비스의 라이프사이클 설계를 참조하세요.
설정 덮어쓰기와 배포 시 캐시 갱신은 패키지 설정의 병합과 캐시에서 다룹니다. 베이스 URL은 신뢰할 수 있는 애플리케이션 설정으로 관리하고, 요청 입력으로 접속 대상을 자유롭게 지정하게 하지 마세요.
실제 클라이언트를 fake로 검증하기
클라이언트 자체를 모킹하면 공유 Factory를 사용하고 있는지, URL이나 헤더를 올바르게 조립하고 있는지는 확인할 수 없습니다. 패키지 테스트에서는 실제CatalogClient를 컨테이너에서 해석하고, HTTP 응답만 fake로 만듭니다.
다음은 Orchestra Testbench의 테스트 환경에서 실행하는 예입니다. 각 테스트의 setUp()에서 preventStrayRequests()를 활성화합니다. 통신하는 테스트에서는 예상 URL에만 응답을 설정하고 '*' 폴백을 두지 않음으로써 URL의 오류도 검출할 수 있습니다.
assertSent()는 일치하는 요청이 1건 이상 있는지를 검증합니다. 의도하지 않은 추가 전송을 놓치지 않도록 assertSentCount()도 함께 사용합니다. assertSentInOrder()는 건수와 순서를 검증하므로, 같은 클라이언트를 연속으로 사용했을 때의 상태 분리 확인에도 활용할 수 있습니다.
가드 설정은 parent::setUp() 이후에 합니다. 이 테스트만으로 프로바이더 부팅 중의 통신까지 검증할 수 있는 것은 아니므로, 등록과 부팅에서는 통신하지 않는 설계를 유지합니다.
fake로 확인할 수 있는 것은 클라이언트가 조립한 요청과 준비한 응답에 대한 처리입니다. 실제 서비스의 인증, TLS 접속, 실제 타임아웃 시간, 외부 API의 사양 변경까지 보장하는 것은 아닙니다. 외부 접속을 수반하는 검증은 명시적으로 준비한 검증 환경에서 별도로 실시합니다.
장기 유지보수를 위해 추가할 검증
공유 Factory와 각 PendingRequest의 책임을 나눠 두면, 사용하는 쪽이 일반적인 Laravel 테스트 방법을 그대로 사용할 수 있습니다. 독자적인 fake 구조를 먼저 만들기보다, 우선 공유 Factory를 거치는 구현과 이를 확인하는 테스트를 준비하세요.
참조한 1차 정보
- Laravel 공식 문서: HTTP 클라이언트
- Laravel 공식 문서: 패키지 개발
- Framework v13.39.0: Factory의 fake·createPendingRequest·__call
- Framework v13.39.0: PendingRequest의 설정과 stub 처리
- Framework v13.39.0: Response의 throw와 JSON 디코드
- Framework v13.39.0: Http 파사드의 해석 대상과 fake
- Framework v13.39.0: FoundationServiceProvider의 singleton 등록
- Laravel 공식 테스트: HttpClientTest