미래 지향적인 웹 앱 구축: The Codest의 전문가 팀이 제공하는 인사이트
The Codest가 최첨단 기술로 확장 가능한 대화형 웹 애플리케이션을 제작하고 모든 플랫폼에서 원활한 사용자 경험을 제공하는 데 탁월한 성능을 발휘하는 방법을 알아보세요. Adobe의 전문성이 어떻게 디지털 혁신과 비즈니스를 촉진하는지 알아보세요...
코데스트는 주로 Ruby를 사용하지만, 우리가 구축 중인 많은 프로젝트 중 하나는 JavaScript입니다. 이 라이브러리는 클라이언트 측 라이브러리로, 아주 오래된 브라우저를 포함하여 현존하는 거의 모든 브라우저를 지원해야 하고, 게다가 수많은 외부 스크립트 및 서비스와 상호 작용해야 하는 매우 까다로운 환경에서 실행됩니다. 정말 재미있죠.
위의 요구 사항에는 일반적으로 클라이언트 측에 존재하지 않는 일련의 문제가 있습니다. 프로젝트그리고 이러한 문제 중 하나는 테스트와 관련이 있습니다. 물론 완벽한 단위 테스트 세트를 보유하고 있으며 CI/CD 환경에서 매우 큰 브라우저/운영 체제 조합 매트릭스에 대해 테스트를 실행하고 있지만 그것만으로는 잘못될 수 있는 모든 문제를 파악할 수 없습니다.
우리가 실행 중인 에코시스템의 중요한 아키텍처로 인해 일부 외부 라이브러리는 우리 라이브러리와 함께 로드되는 것에 의존하고 있습니다. 코드할 수도 없고 할 수 있는 일도 없습니다. 이러한 라이브러리는 흥미로운 도전 과제입니다:
이는 단위 테스트가 충분하지 않은 이유를 명확하게 보여줍니다. 단위 테스트는 실제 세계와 분리된 상태에서 테스트하기 때문입니다. 문서에서 발견한 내용을 기반으로 외부 라이브러리의 공용 API의 일부를 모방하고 이에 대해 단위 테스트를 실행한다고 가정해 보겠습니다. 그러면 무엇을 증명할 수 있을까요?
"외부 라이브러리의 API와 함께 작동한다는 뜻인가?"라고 반문하고 싶으실 수도 있지만, 안타깝게도 이는 잘못된 생각입니다. 외부 라이브러리의 공용 API의 하위 집합과 올바르게 상호 작용한다는 의미일 뿐이며, 그마저도 저희가 모방한 버전과만 상호 작용합니다.
라이브러리가 말 그대로 우리 눈앞에서 바뀌면 어떻게 될까요? 야생에서 문서화되지 않은 다른 코드 경로로 이동하는 이상한 응답이 발생하면 어떻게 될까요? 이를 방지할 수 있을까요?
100%가 아니라 환경이 너무 복잡하기 때문입니다. 하지만 실제 코드에서 발생할 수 있는 몇 가지 일반화된 예시를 통해 모든 것이 정상적으로 작동하는지 합리적으로 확인할 수 있는데, 바로 통합 테스트를 수행하는 것입니다. 단위 테스트는 코드가 내부적으로 제대로 실행되는지 확인하고, 통합 테스트는 우리가 제어할 수 없는 라이브러리와 제대로 '대화'하는지 확인해야 합니다. 그리고 그 라이브러리의 스텁이 아니라 실제 라이브 라이브러리를 사용해야 합니다.
다음에 대해 사용 가능한 통합 테스트 프레임워크 중 하나를 사용할 수 있습니다. JavaScript를 사용하여 간단한 HTML 페이지를 만들고, 라이브러리와 원격 라이브러리에 몇 가지 호출을 던지고, 잘 작동하는지 테스트해 볼 수 있습니다. 하지만 CI/CD 환경에서 생성된 호출로 인해 원격 서비스의 엔드포인트가 넘쳐나고 싶지는 않습니다. 일부 통계가 엉망이 되고, 일부가 깨질 수도 있으며, 마지막으로 누군가의 프로덕션을 테스트의 일부로 만드는 것은 좋지 않을 것입니다.
하지만 이렇게 복잡한 통합 테스트가 가능할까요? 저희는 루비를 가장 좋아하기 때문에 전문 지식을 바탕으로 루비 프로젝트에서 원격 서비스와의 통합 테스트를 일반적으로 어떻게 수행하는지 생각하기 시작했습니다. 우리는 다음과 같은 것을 사용할 수 있습니다. vcr 보석으로 한 번 녹화한 다음 필요할 때마다 테스트에 계속 재생할 수 있습니다.
내부적으로 vcr은 요청을 프록시하여 이를 달성합니다. 그때가 바로 '아하!'의 순간이었습니다. '실제' 인터넷에 닿지 않아야 하는 모든 요청을 일부 스텁 응답으로 프록시해야 했습니다. 그러면 들어오는 데이터가 외부 라이브러리로 전달되고 코드는 평소처럼 실행됩니다.
복잡해 보이는 프로토타입을 만들 때 시간을 절약하기 위해 루비를 사용하는 경우가 많습니다. 우리는 (아마도) JavaScript에서 더 복잡한 것을 만들기 전에 프록시 아이디어가 얼마나 잘 작동하는지 확인하기 위해 Ruby로 JavaScript용 프로토타입 테스트 하네스를 만들기로 결정했습니다. 의외로 간단한 것으로 밝혀졌습니다. 사실 너무 간단해서 이 글에서 함께 만들어 보려고 합니다.
물론 우리가 만들고 있는 것을 조금이라도 설명하는 것은 블로그 게시물의 범위를 훨씬 넘어서는 것이므로 '진짜'를 다루지는 않을 것입니다. 문제의 라이브러리를 대신할 수 있는 빠르고 쉬운 것을 만든 다음 Ruby 부분에 더 집중할 수 있습니다.
먼저, 우리가 다루고 있는 외부 라이브러리를 대신할 무언가가 필요합니다. 외부 서비스와 접촉하고, 여기저기서 이벤트를 발생시켜야 하며, 무엇보다도 쉬운 통합을 염두에 두고 구축되어서는 안 된다는 몇 가지 동작을 보여줄 필요가 있습니다 🙂.
사용 방법은 다음과 같습니다:
/* global XMLHttpRequest, Event */
const URL = 'https://poloniex.com/public/?command=returnTicker'
const METHOD = 'GET'
module.exports = {
fetch: function () {
var req = new XMLHttpRequest()
req.responseType = 'json'
req.open(METHOD, URL, true)
var doneEvent = new Event('example:fetched')
req.onreadystatechange = function (aEvt) {
if (req.readyState === 4) {
if (req.status === 200) {
this.data = req.response
} else {
this.error = true
}
window.dispatchEvent(doneEvent)
}
}.bind(this)
req.send(null)
},
error: false,
data: {}
}
일부 데이터(이 경우에는 요즘 대세인 암호화폐 환율)에 대해 오픈 API를 호출하는 것을 볼 수 있습니다. 이 API는 샌드박스를 노출하지 않으며 환율 제한이 있으므로 테스트에서 실제로 부딪히지 않아야 하는 대표적인 예입니다.
이 모듈이 실제로는 NPM 호환 모듈이라는 것을 눈치챘을 수도 있지만, 쉬운 번들링을 위해 일반적으로 처리하는 스크립트는 NPM에서 사용할 수 없다는 것을 암시했습니다. 이 데모에서는 특정 동작을 보여주는 것으로 충분하며, 지나치게 단순화하는 대가를 치르고서라도 쉽게 설명하고자 합니다.
이제 라이브러리를 대신할 무언가가 필요합니다. 다시 말하지만, "외부" 라이브러리를 호출하고 출력으로 무언가를 수행해야 합니다. "테스트 가능한" 부분을 단순하게 유지하기 위해 사양에서 읽기 어려운 콘솔과 전역적으로 사용 가능한 배열에 모두 이중 로깅을 수행하도록 하겠습니다.
window.remote = require('remote-calling-example')
window.failedMiserably = true
window.logs = []
함수 로그 (메시지) {
window.logs.push(message)
console.log(message)
}
window.addEventListener('example:fetched', function () {
if (window.remote.error) {
log('[EXAMPLE] 원격 페치 실패')
window.failedMiserably = true
} else {
log('[EXAMPLE] 원격 불러오기 성공')
log([예시] BTC에서 ETH로: ${window.remote.data.BTC_ETH.last})
}
})
window.remote.fetch()
또한 일부러 동작을 엄청나게 단순하게 유지했습니다. 이대로라면 실제로 흥미로운 코드 경로가 두 개밖에 없기 때문에 빌드를 진행하면서 수많은 스펙에 휩쓸리지 않을 것입니다.
간단한 HTML 페이지를 만들어 보겠습니다:
<code> <!DOCTYPE html>
<html>
<head>
<title>예제 페이지</title>
<script type="text/javascript" src="./index.js"></script>
</head>
<body></body>
</html>
이 데모에서는 HTML과 JavaScript를 다음과 함께 번들로 제공하겠습니다. 소포매우 간단한 웹 앱 번들러입니다. 저는 간단한 예제를 만들거나 백오브냅킨 클래스 아이디어를 해킹할 때 Parcel을 많이 사용합니다. 웹팩을 구성하는 것이 원하는 코드를 작성하는 것보다 더 오래 걸릴 정도로 간단한 작업을 할 때 가장 좋습니다.
또한 눈에 잘 띄지 않아서 좀 더 실전 테스트를 거친 것으로 전환하고 싶을 때 Parcel에서 거의 백페달링을 할 필요가 없는데, 이는 웹팩에 대해 말할 수 있는 부분이 아닙니다. 하지만 주의할 점은 Parcel은 개발이 한창 진행 중이며 문제가 발생할 수 있고 앞으로도 계속 발생할 수 있다는 것입니다. Node.js. 결론: 아직 프로덕션 파이프라인의 일부로 삼지는 마시고, 그래도 한 번 사용해 보세요.
이제 테스트 하네스를 구성할 수 있습니다.
사양 프레임워크 자체의 경우 다음을 사용했습니다. rspec. 개발 환경에서는 헤드리스가 없는 실제 Chrome을 사용하여 테스트합니다. watir (그리고 믿을 수 있는 조력자 watir-rspec입니다). 프록시를 위해 다음을 초대했습니다. 퍼핑 빌리 그리고 랙 를 파티에 추가합니다. 마지막으로, 사양을 실행할 때마다 JavaScript 빌드를 다시 실행하고 싶습니다. 코카인.
이 간단한 예제에서도 사양 도우미가 다소... 관여하는 부분이 많기 때문입니다. 한 번 살펴보고 분해해 보겠습니다.
Dir['./spec/support/*/.rb'].each { |f| require f }
TEST_LOGGER = Logger.new(STDOUT)
RSpec.config do |config|
config.before(:suite) { Cocaine::CommandLine.new('npm', 'run build', logger: TEST_LOGGER).run }
config.include Watir::RSpec::Helper
config.include Watir::RSpec::Matchers
config.include ProxySupport
config.order = :random
BrowserSupport.config(config)
end
Billy.config do |c|
c.cache = false
c.cacherequestheaders = false
c.persistcache = false
c.recordstubrequests = true
c.logger = Logger.new(File.expandpath('../log/billy.log', FILE))
end
전체 제품군 전에 코카인을 통해 커스텀 빌드 명령을 실행합니다. 테스트_로거 상수는 약간 과할 수 있지만 여기서는 객체 수에 크게 신경 쓰지 않습니다. 물론 무작위 순서로 사양을 실행하고 있으며, watir-rspec의 모든 좋은 점을 포함해야 합니다. 또한 캐싱을 수행하지 않고 다음에 대한 광범위한 로깅을 수행하도록 Billy를 설정해야 합니다. spec/log/billy.log
. 요청이 실제로 스텁되고 있는지 또는 라이브 서버에 문제가 있는지(웁스!) 알 수 없다면 이 로그는 순금과도 같습니다.
여러분의 예리한 눈은 이미 프록시 지원과 브라우저 지원을 발견했을 것입니다. 그 안에 저희의 맞춤 기능이 들어 있다고 생각하실 수도 있습니다... 그리고 그 말이 맞습니다! 먼저 BrowserSupport가 어떤 기능을 하는지 살펴봅시다.
먼저 다음을 소개합니다. 임시 브라우저
:
TempBrowser 클래스
def get
@browser ||= Watir::Browser.new(web_driver)
end
def kill
브라우저.close if @browser
브라우저 = nil
end
private
def web_driver
셀레늄::웹드라이버.for(:크롬, 옵션: 옵션)
end
def 옵션
셀레늄::웹드라이버::크롬::옵션.new.탭 do |옵션|
options.addargument '--auto-open-devtools-for-tabs'
options.addargument "--proxy-server=#{Billy.proxy.host}:#{Billy.proxy.port}"
end
end
end
호출 트리를 거꾸로 살펴보면 Chrome용 Selenium 브라우저 옵션을 설정하고 있음을 알 수 있습니다. 이 옵션 중 하나는 설정에 매우 중요한 옵션으로, 크롬 인스턴스가 모든 것을 퍼핑 빌리 인스턴스를 통해 프록시하도록 지시합니다. 다른 옵션은 그냥 있으면 좋은 옵션입니다. 우리가 실행하는 모든 인스턴스가 헤드리스 를 누르면 검사 도구가 자동으로 열립니다. 덕분에 하루에 셀 수 없을 정도로 많은 Cmd+Alt+I를 절약할 수 있습니다 😉.
이러한 옵션으로 브라우저를 설정한 후 Watir에 전달하면 끝입니다. 브라우저의 kill
메서드는 임시 브라우저 인스턴스를 버리지 않고 필요한 경우 드라이버를 반복적으로 중지하고 다시 시작할 수 있는 약간의 설탕입니다.
이제 rspec 예제에 몇 가지 초능력을 부여할 수 있습니다. 우선, 우리는 멋진 브라우저
헬퍼 메서드를 주로 사용하게 될 것입니다. 또한 매우 민감한 작업을 수행하는 경우 특정 예제를 위해 브라우저를 다시 시작하는 편리한 방법을 사용할 수도 있습니다. 물론 테스트 스위트가 완료된 후에는 브라우저를 종료하는 것이 좋습니다. 어떤 상황에서도 RAM을 위해 크롬 인스턴스가 남아 있는 것을 원하지 않기 때문입니다.
모듈 브라우저 지원
def self.browser
@browser ||= TempBrowser.new
end
def self.configure(config)
config.around(:각) do |example|
BrowserSupport.browser.kill if example.metadata[:clean]
@browser = BrowserSupport.browser.get
@browser.cookies.clear
브라우저.드라이버.관리.시간 초과.암시적_대기 = 30
example.run
end
config.after(:suite) do
BrowserSupport.browser.kill
end
end
end
브라우저와 스펙 헬퍼를 설정했고 프록시로 요청을 프록시로 전송할 준비가 되었습니다. 하지만 잠깐만요, 아직 설정하지 않았습니다! 모든 예제에 대해 Billy에게 반복적으로 호출할 수도 있지만, 몇 가지 헬퍼 메서드를 사용하여 몇 천 번의 키 입력을 절약하는 것이 더 낫습니다. 바로 프록시 지원
를 사용합니다.
테스트 설정에 사용한 설정은 약간 더 복잡하지만 일반적인 아이디어는 다음과 같습니다:
냉동 문자열 문자: true
'json' 필요
모듈 ProxySupport
HEADERS = {
'Access-Control-Allow-Methods' => 'GET',
'Access-Control-Allow-Headers' => 'X-요청-함께, X-프로토타입 버전, 콘텐츠 유형',
'Access-Control-Allow-Origin' => '*'
}.freeze
def stubjson(url, file)
Billy.proxy.stub(url).andreturn({
body: open(file).read,
code: 200,
headers: HEADERS.dup
})
end
def stubstatus(url, status)
Billy.proxy.stub(url).andreturn({
body: '',
code: status,
headers: HEADERS.dup
})
end
def stubpage(url, file)
Billy.proxy.stub(url).andreturn(
body: open(file).read,
content_type: 'text/html',
code: 200
)
end
def stubjs(url, file)
Billy.proxy.stub(url).andreturn(
body: open(file).read,
content_type: 'application/javascript',
code: 200
)
end
end
멈출 수 있습니다:
이것은 간단한 예제에 적합합니다. 예제 말이 나와서 말인데 몇 가지를 설정해 보겠습니다!
먼저 프록시를 위한 몇 가지 '경로'를 연결해야 합니다:
let(:pageurl) { 'http://myfancypage.local/index.html' }
let(:jsurl) { 'http://myfancypage.local/dist/remote-caller-example.js' }
let(:pagepath) { './dist/index.html' }
let(:jspath) { './dist/remote-caller-example.js' }
전에
스텁페이지 페이지URL, 페이지경로
stubjs jsurl, jspath
end
여기서 상대 경로는 기본 프로젝트 디렉터리를 참조하므로, rspec의 관점에서 볼 때 HTML과 JS를 바로 dist
디렉토리에 저장됩니다. 이미 이러한 stub_*
도우미가 유용합니다.
또한 '가짜' 웹사이트를 다음과 같은 웹사이트에 배치하고 있다는 점도 주목할 가치가 있습니다. .local
TLD. 이렇게 하면 문제가 발생해도 런어웨이 요청이 로컬 환경을 벗어나지 않습니다. 일반적으로 꼭 필요한 경우가 아니라면 스텁에 '실제' 도메인 네임을 사용하지 않는 것이 좋습니다.
여기서 주의해야 할 또 다른 사항은 반복하지 말아야 한다는 것입니다. 프록시 라우팅이 더 복잡해지고 경로와 URL이 많아지면 이 설정을 공유 컨텍스트로 추출하여 필요에 따라 간단히 포함시키는 것이 실질적인 가치가 있을 것입니다.
이제 '좋은' 경로가 어떤 모습이어야 하는지 구체화할 수 있습니다:
문맥 '정답으로' do
전에 do
stubjson %r{http://poloniex.com/public(.*)}, './spec/fixtures/remote.json'
페이지로 이동
Watir::Wait.until { browser.execute_script('return window.logs.length === 2') }
end
'적절한 데이터를 기록합니다' do
expect(browser.execute_script('return window.logs')).to(
eq(['[예시] 원격 가져오기 성공', '[예시] BTC에서 ETH로: 0.03619999'])
)
end
end
꽤 간단하지 않나요? 여기서 몇 가지 설정이 더 필요합니다. 원격 API의 JSON 응답을 픽스처로 스텁하고 기본 URL로 이동한 다음... 기다립니다.
기다림은 Watir에서 직면한 한계를 해결하는 방법입니다. 예를 들어 JavaScript 이벤트를 안정적으로 기다릴 수 없으므로 스크립트에서 액세스할 수 있는 객체가 관심 있는 상태로 이동할 때까지 약간의 치트를 사용하여 "기다려야" 합니다. 단점은 해당 상태가 오지 않을 경우(예를 들어 버그로 인해) 와티르 웨이터가 시간 초과될 때까지 기다려야 한다는 것입니다. 이로 인해 스펙 시간이 약간 늘어납니다. 그래도 사양은 여전히 안정적으로 실패합니다.
페이지가 우리가 관심 있는 상태로 '안정화'되면 페이지의 컨텍스트에서 JavaScript를 더 실행할 수 있습니다. 여기서 퍼블릭 배열에 기록된 로그를 불러와서 예상했던 것과 일치하는지 확인합니다.
여담이지만, 여기서 원격 요청 스텁이 정말 빛을 발합니다. 콘솔에 기록되는 응답은 원격 API가 반환하는 환율에 따라 달라지므로 로그 내용이 계속 변경되면 안정적으로 테스트할 수 없습니다. 물론 이 문제를 해결할 수 있는 방법이 있지만 그다지 우아하지는 않습니다.
테스트할 것이 하나 더 있습니다: '실패' 브랜치입니다.
컨텍스트 '실패한 응답으로' do
전에 do
스텁 상태 %r{http://poloniex.com/public(.*)}, 404
페이지로 이동
Watir::Wait.until { browser.execute_script('return window.logs.length === 1') }
end
'로그 실패' do
expect(browser.execute_script('return window.logs')).to(
eq(['[EXAMPLE] 원격 가져오기 실패'])
)
end
end
위와 매우 유사하지만, 404 HTTP 상태 코드를 반환하도록 응답을 스텁하고 다른 로그를 기대한다는 차이점이 있습니다.
이제 사양을 실행해 보겠습니다.
% 번들 실행 rspec
시드 63792로 무작위화
I, [2017-12-21T14:26:08.680953 #7303] INFO -- : 명령 :: npm run build
원격 호출
올바른 응답으로
올바른 데이터를 기록합니다.
실패한 응답으로
실패 로그
23.56초 만에 완료(파일 로드에 0.86547초 소요)
예제 2개, 실패 0건
우후!
JavaScript를 Ruby와 통합 테스트하는 방법에 대해 간략하게 설명했습니다. 원래는 임시방편에 가깝다고 생각했지만, 지금은 작은 프로토타입에 꽤 만족하고 있습니다. 물론 여전히 순수한 JavaScript 솔루션을 고려하고 있지만, 그 동안에는 야생에서 마주쳤던 매우 복잡한 상황을 재현하고 테스트할 수 있는 간단하고 실용적인 방법이 생겼습니다.
이와 유사한 것을 직접 구축하려는 경우 한계가 없다는 점에 유의해야 합니다. 예를 들어 테스트하는 내용이 정말 AJAX를 많이 사용하는 경우 퍼핑 빌리는 응답하는 데 시간이 오래 걸립니다. 또한 일부 SSL 소스를 스텁해야 하는 경우 약간의 추가 작업이 필요할 수 있으므로 요구 사항이 있는 경우 와티르 문서를 살펴보세요. 저희는 고유한 사용 사례를 처리할 수 있는 최선의 방법을 계속 모색하고 찾아볼 것이며, 그 결과를 알려드리도록 하겠습니다.