[사내기술교육][강의자료] 프런트엔드 테스팅

김동우 2018.05.10 20:38

첨부파일1frontend-testing-source.zip44.4 KB01.15 10:36

테스트란?어플리케이션이 요구 사항에 맞게 동작하는지를 검증하는 행위테스트의 예DB에 데이터를 입력하는 API를 개발 -> API 호출 -> DB값 검증디자인 시안에 맞게 HTML/CSS를 작성 -> 브라우저에서 실제 렌더링된 결과를 확인새로운 기능을 추가하기 위해 기존 모듈을 리팩토링 -> 영향을 받는 다른 모듈의 실행 결과를 확인버그를 수정하기 위해 기존 함수를 수정 -> 버그가 수정 확인 & 영향을 받는 다른 모듈의 실행 결과를 확인개발 환경에서 테스트된 어플리케이션을 리얼 환경에 배포 -> 배포 과정에서 발생한 문제가 없는지 확인개발과 테스트개발자는 사실상 코드를 작성하는 것보다 더 많은 시간을 테스트에 사용왜 테스트를 작성하는가?테스트 자동화장점사람이 수행해야 하는 반복된 테스트를 자동화 할 수 있음 (비용 감소)사람이 수행하는 것보다 훨씬 빠르게 테스트할 수 있음사람이 수행하는 것보다 더 신뢰할 수 있음단점감각적인 요소(시각, 청각) 등 사용자 경험과 관련된 문제를 찾아낼 수 없음실제 환경에서 벌어지는 다양한 상황을 자동화하기 어려움 (네트워크, 디바이스 관련 등)테스트 자동화는 누가 하는가?QA개발자둘 다 (O)개발자가 테스트 작성해야 하는 이유제품 품질개발자는 작성한 프로그램의 퀄리티에 대한 책임이 있음QA에 넘기기 전에 기본 요구사항을 모두 만족하는지에 대한 검증은 개발자가 해야 함자동화된 테스트를 작성해 두지 않으면, 어플리케이션이 복잡해질 수록 테스트 비용이 증가함이 경우 개발 기간이나 인력 등은 한정되어 있기 때문에, 테스트를 소홀히 하게 되는 경우가 많음.그렇지 않은 경우 QA 와의 커뮤니케이션 비용이 늘어나, 업무 효율이 떨어지게 됨코드 품질코드 품질을 위해서는 계속해서 리팩토링 등의 개선 작업이 필요이 과정에서 기존에 잘 동작하던 프로그램을 망칠 수 있기 때문에 적극적으로 코드를 개선하지 않게 됨신뢰할 수 있는 자동화된 테스트가 있으면 적극적으로 코드를 개선할 수 있음두려움(Fear) -> 자신감(Confidence)TDDTDD : 테스트 주도 개발 (Test Driven Development)테스트를 먼저 작성한 후 구현 코드를 작성하는 개발 방법론실제 구현 코드를 사용하는 입장에서 먼저 생각할 수 있기 때문에 디자인에 도움을 줄 수 있음TDD가 항상 좋은 디자인을 보장하지는 않음 -> 사고를 도와주는 방법론일 뿐TDD !== 자동화 테스트 (TDD !== 단위 테스트)TDD가 적절하지 않은 종류의 프로그램도 있음 (UI 개발 등)When TDD Doesn't work - Uncle Bobnow we have two places where TDD is impractical or inappropriate. The physical boundary, and the layer just in front of that boundary that requires human interaction to fiddle with the results.테스트의 종류분류범위에 따라단위(Unit) 테스트통합(Integation) 테스트E2E(End to End) 테스트기능(Functional) 테스트시스템(System) 테스트UI(User Interface) 테스트그 외회귀(Regression) 테스트성능(Performance) 테스트단위(Unit) 테스트특징모듈(함수/클래스) 단위의 테스트테스트할 부분의 코드를 다른 시스템으로부터 분리(isloate)시킨 채 테스트작성 비용이 적게 들고 실행 속도가 빠름실패했을 때 문제가 생긴 부분을 비교적 정확하게 파악할 수 있음경우에 따라 한두개의 단위를 모아서 하나의 단위로 취급하기도 함Sociable vs SolitarySociable Tests : 의존성이 있는 다른 코드들과 함께 테스트Solitary Tests : 테스트 더블을 이용해 완벽하게 분리시킨 채 테스트경우에 따라 적절한 방법을 사용참고https://martinfowler.com/bliki/UnitTest.html통합 테스트특징주로 단위 테스트보다 큰 범위의 테스트를 의미개별 모듈(함수/클래스)들이 연결되어 제대로 상호작용하는지를 테스트단위 테스트에 비해 작성이 어렵고 실행 속도가 느림단위 테스트에 비해 실패 시 문제가 생긴 부분을 정확히 파악하기가 어려움Narrow vs Broad좁은 통합 테스트 : 테스트 더블을 이용해 외부 서비스를 실제 구동하지 않고 테스트넓은 통합 테스트 : 의존성이 있는 모든 외부 서비스를 사용하여 테스트참고https://martinfowler.com/bliki/IntegrationTest.htmlAlthough I prefer to focus the definition on the interaction of separately built modules, I do occasionally see “integration test” used to mean anything bigger than a unit test. And for some users of solitary unit tests, I’ve seen them describe sociable unit tests as “integration tests”.E2E 테스트실제 사용자가 사용하는 것과 같은 조건에서 전체 시스템을 테스트API 서버, DB 등의 외부 서비스들을 모두 사용하여 통합된 시스템을 테스트단위/통합 테스트에 비해 작성이 어렵고 실행 속도가 가장 느림문제가 생긴 부분을 정확히 파악하기가 가장 어려움기능(Functional) 테스트와 비슷한 의미로 사용됨(G)UI 테스트백엔드 시스템을 Mocking한 채 UI만 테스트할 수도 있음테스트 피라미드**(출처 : https://martinfowler.com/bliki/TestPyramid.html)테스트 토로피(?)(출처 : https://twitter.com/kentcdodds/status/960723172591992832)단위 vs 통합 테스트 (GIF 1)단위 vs 통합 테스트 (GIF 2)단위 vs 통합 테스트Why Most Unit Testing is WasteWhy Most Unit Testing is Waste — Tests Don’t Improve Quality: Developers DoA Response to “Why Most Unit Testing is Waste”Just Say No to More End-to-End Tests자바스크립트 테스팅 환경테스트 러너테스트 파일을 읽어들여 실행하고, 결과를 출력파일이 변경된 경우 자동으로 실행해주는 watch 등의 기능 제공Reporter를 지정해서 원하는 형태로 결과를 출력Node 환경에서 실행 : Mocha, Jest, AVA브라우저 환경에서 실행 : Karma테스트 프레임워크사용자가 테스트 코드를 작성할 수 있는 기반을 제공프레임워크에 의해 제공된 함수를 이용해서 테스트 코드를 작성-> 프레임워크가 자동으로 테스트를 실행하고 결과를 수집해서 출력테스트 Spec 들을 그룹핑하거나 공통 사전 작업 등을 처리해 줌Mocha, Jasmine (Jest), AVA예제 (Jasmine)describe('calculations', () => { let a, b beforeEach(() => { a = 10; b = 20; }); it('sum two number', () => { expect(a + b).toBe(30); }); it('multiply two number', () => { expect(a * b).toBe(200); }); }); 단언(Assertion)다양한 스타일의 Assertion을 사용할 수 있도록 API를 제공대부분 테스트 프레임워크에 포함된 형태로 사용 (Jasmine, Jest, AVA)Mocha의 경우 별도의 라이브러리인 Chai를 사용예제 (Jasmine)expect(obj).not.toBeNull(); expect(obj).toEqual({ name: 'Kim', age: 30 }); expect(result).toBe(true); expect(result).toBeTruthy(); expect(spy).toHaveBeenCalled(); 테스트 더블테스트 더블이란?https://martinfowler.com/bliki/TestDouble.html테스트를 하기 위해 실제 코드 대신에 사용하는 객체/모듈/함수.Dummy, Stub, Mock, Spy 등을 통칭테스트 더블을 작성하기 쉽게 도와주는 라이브러리Sinon, Jasmine, Jestconst person = { name: 'Kim', getName() { return this.name; }, setName(name) { this.name = name; } } it('test spy', () => { spyOn(person, 'setName'); spyOn(person, 'getName').and.callThrough(); person.setName('Lee'); const name = person.getName(); expect(person.setName).toHaveBeenCalledWith('Lee'); expect(person.getName).toHaveBeenCalled(); expect(name).toBe('Kim'); }); 브라우저 환경 vs Node.js 환경브라우저에서 테스트 실행실제 브라우저 환경에서 테스트 코드를 실행 (Karma + Jasmine)실제 브라우저를 실행해야 하기 때문에 번거로움 (Headless 브라우저 사용)테스트파일 별로 별도의 브라우저에서 테스트 하기가 어려움 (속도 문제)빈 웹페이지를 만들고 모든 스크립트 파일 및 CSS 등을 include 해서 테스트 (번들 과정 필요)브라우저의 모든 API를 사용해서 테스트 가능브라우저 호환성 테스트 가능개발시 : 빠른 Feedback을 위해 Headless 브라우저를 사용빌드시 : CI 서버 및 Webdriver와 연동하여 여러개의 브라우저에서 테스트Node.js에서 테스트 실행Node.js 환경에서 테스트 코드를 실행 (Mocha, Jest 등)브라우저에 비해서 가볍기 때문에 실행속도가 빠름개별 테스트 파일을 별도의 프로세스에서 실행할 수 없음 (병렬 실행 가능)브라우저 API 대신 JSDom 을 이용해서 테스트실제 렌더링을 해 주지 않으므로, 렌더링 관련 테스트 불가능브라우저 호환성 테스트 불가능2019 State of Javascript테스팅 도구 만족도 변화 (2016 ~ 2019)https://2019.stateofjs.com/testing/실습 : Jest 설치 및 사용Jest페이스북에서 만든 자바스크립트 테스팅 라이브러리. 오픈소스(MIT)현재 페이스북 내의 모든 자바스크립트 테스트에 사용됨테스트 러너 / 구조화 / 단언 / 테스트 더블 등의 기능을 모두 포함Node 환경에서 JSDom을 이용해 테스트 (브라우저 테스트 불가)테스트를 병렬로 수행해서 속도를 높임Jasmine 과 호환되는 단언 API 형식 (처음엔 Jasmine 사용 -> 현재 자체 구현)Zero Configuration : 설정 없이 간단하게 실행할 수 있음Why?단위/통합 테스트를 실제 브라우저에서 실행해야 할 이유가 많이 사라짐Babel 등의 트랜스파일러가 보편화되고, 브라우저간 차이가 줄어듬단위 테스트는 빠른 피드백이 중요 -> 가볍고 빠른 Node 환경이 적합개별 테스트를 별도의 프로세스에서 실행 -> 안전한 테스트 환경모듈 Mocking, 스냅샷 테스팅가장 활발하게 발전하고 있고, 페이스북의 지원이 있음E2E 테스트 도구와 함께 사용하면 단점 보완 가능test / it실제 테스트가 실행될 함수를 등록test 와 it 은 이름만 다르고 기능은 동일test('테스트 내용', () => { // 테스트 코드 }); it('테스트 내용', () => { // 테스트 코드 }); describe테스를 구조화하기 위해 사용관련 있는 테스트들끼리 그룹으로 묶어줌describe('모듈 A', () => { test('테스트 1', () => { // ... }); test('테스트 2', () => { // ... }); }); describe('모듈 B', () => { test('테스트 1', () => { // ... }); test('테스트 2', () => { // ... }); }); expect()값을 검증하기 위한 다양한 매처(matcher) 들을 제공toBe() / toEqual()test('원시 타입인 경우 toBe/toEqual 모두 값 비교를 한다', () => { expect(true).toBe(true); expect(true).toEqual(true); expect(1 + 2).toBe(3); expect(1 + 2).toEqual(3); expect('3').not.toBe(3); expect('3').not.toEqual(3); });; describe('객체인 경우', () => { const artist1 = { name: { first: 'Michael', last: 'Jackson' }, songs: [ 'Beat It', 'Man in the Mirror' ] } const artist2 = { name: { first: 'Michael', last: 'Jackson' }, songs: [ 'Beat It', 'Man in the Mirror' ] } test('toBe는 참조 비교를 한다.', () => { expect(artist1).toBe(artist1); expect(artist1).not.toBe(artist2); }); test('toEqual은 값 비교를 한다.', () => { expect(artist1).toEqual(artist2); }); }); 실습 내용실습 목표describe / expect / toBe / toEqual 등의 사용 방법을 익힌다.Math.round() 함수에 대한 테스트를 작성한다.Array.prototype.slice() 함수에 대한 테스트를 작성한다.진행 방법테스트 파일 : test/basic.spec.js스크립트 : npm run test:basic실습 : TDD (add / swap)TDDRed - Green - RefactorRed : 실패하는 테스트 케이스를 작성Green : 해당 테스트 케이스가 성공하도록 코드를 작성Refactor: 기능을 유지한 채 내부 구조와 코드를 개선실습 내용목표TDD 방식으로 add 함수와 swap 함수를 구현한다.진행 방법스크립트 : npm run test:util테스트 파일 : test/util.spec.js소스 파일 : src/util.js실습 : TDD (counter)Setup / TeardownbeforeEach각각의 테스트가 실행되기 전에 실행됨테스트를 위한 Setup 작업을 수행하는 코드를 작성afterEach각각의 테스트가 끝난 후에 실행됨테스트 이후에 초기 상태를 복구하는 등의 코드를 작성예제beforeEach(() => { console.log('before outer'); }); afterEach(() => { console.log('after outer'); }); test('test outer', () => { console.log('test outer'); }); describe(() => { beforeEach(() => { console.log('before inner'); }); afterEach(() => { console.log('after inner'); }); test('test inner', () => { console.log('test inner'); }); }); before outer test outer after outer before outer before inner test inner after inner after outer 실습 내용목표TDD 방식으로 상태를 갖는 간단한 객체(counter)를 구현한다.메소드 단위가 아닌 기능 단위로 스펙을 작성하는 방법을 익힌다beforeEach / afterEach를 이용하여 반복된 Setup 코드를 정리하는 법을 익힌다.진행 방법스크립트 : npm run test:counter테스트 파일 : test/counter.spec.js소스 파일 : src/counter.jsAPIimport {createCounter} from '../src/counter'; const counter = createCounter(); counter.val(); // 0 counter.inc(); counter.inc(); counter.val(); // 2 counter.dec(); counter.val(); // 1 API (options)import { createCounter } from '../src/counter'; const counter = createCounter({ initVal: 5, min: 4, max: 6 }); counter.val(); // 5 counter.isMax(); // false counter.isMin(); // false counter.inc(); counter.inc(); counter.val(); // 6 counter.isMax(); // true counter.dec(); counter.dec(); counter.val(); // 4 counter.isMin(); // true 테스팅 전략좋은 테스트란?1. 실행 속도가 빨라야 한다.빠른 피드백 -> 개발 속도를 빠르게 해 줌너무 느리면 테스트를 자주 실행하지 않게 됨2. 내부 구현 변경 시 실패하지 않아야 한다.리팩토링할 때 테스트가 깨진다면? -> 오히려 코드 개선을 방해구현 종속적인 테스트를 작성하지 않는다내부 구현을 모른채 테스트를 작성(BlackBox 테스팅)인터페이스를 기준으로 테스트를 작성한다.자주 변하는 로직과 변하지 않는 로직을 구분 (ex: 모델과 뷰를 분리)3. 버그를 검출할 수 있어야 한다.소스 코드에 버그가 있어도 검출하지 못한다면 잘못된 테스트테스트가 기대하는 결과를 구체적으로 명시하지 않으면 버그를 검출할 수 없음테스트 더블의 사용을 최소화한다. -> 과하게 사용하면 연결 과정에서의 버그를 검출할 수 없음4. 테스트의 결과가 안정적이어야 한다.특정 환경에서만 실패하거나, 간헐적으로 결과가 달라지는 테스트는 신뢰할 수가 없음외부 환경의 영향을 최소화해서 동일한 결과를 최대한 보장할 수 있어야 함현재 시간, 네트워크 상태, 외부 프로세스 등은 모의 객체나 별도의 도구를 사용해서 직접 조작할 수 있어야 함.5. 의도가 명확히 드러나야 한다.가독성 : "기계가 읽기 좋은 코드" -> "사람이 읽기 좋은 코드"테스트 코드도 실제 코드와 동일한 기준으로 품질 관리를 해야 함테스트 코드를 보고 한 눈에 어떤 내용을 테스트하는지를 파악할 수 있어야 함.공통 로직, Fixture, Mock 등은 분리해서 관리테스팅 ROI테스트 코드 작성과 유지보수는 비용이다테스트가 없는 것보다는 있는 게 무조건 낫다?테스트는 많을 수록 좋다?불필요한 테스트나 잘못 짜여진 테스트는 차라리 없는게 나음비용 대비 효과가 충분한가?테스트를 작성하는 비용에 비해 얻을 수 있는 효과가 더 큰가가 중요로직이 거의 없는(trivial) 코드는 따로 테스트하지 않아도 됨동어 반복적인 테스트를 피하자테스트 범위에 대한 조절이 필요 (단위 테스트 vs 통합 테스트 vs E2E 테스트)모든 모듈에 대해 단위 테스트를 작성하는 것은 비효율적모든 테스트 케이스를 E2E 테스트로만 검증하는 것도 비효율적커버리지 100%를 목표로 하는 것은 비효율적라이브러리 등은 100% 커버리지 가능복잡한 어플리케이션의 경우 적절한 선을 잘 찾는 것이 중요테스팅 도구와 테스팅 방법론은 아직 성숙한 상태가 아님특정 방법론이나 도구에 집착하지 말 것발전하는 테스팅 도구들을 눈여겨 볼 필요도 있음커버리지 100%?https://youtu.be/cAKYQpTC7MA?t=440Kent Beck - Test Desderatahttps://medium.com/@kentbeck_7670/test-desiderata-94150638a4b3Isolatedtests should return the same results regardless of the order in which they are run.Composableif tests are isolated, then I can run 1 or 10 or 100 or 1,000,000 and get the same results.Fasttests should run quickly.Inspiringpassing the tests should inspire confidenceWritabletests should be cheap to write relative to the cost of the code being tested.Readabletests should be comprehensible for reader, invoking the motivation for writing this particular test.Behavioraltests should be sensitive to changes in the behavior of the code under test. If the behavior changes, the test result should change.Structure-insensitivetests should not change their result if the structure of the code changes.Automatedtests should run without human intervention.Specificif a test fails, the cause of the failure should be obvious.Deterministicif nothing changes, the test result shouldn’t change.Predictiveif the tests all pass, then the code under test should be suitable for production.Kent Beck - “I get paid for code that works, not for tests”https://stackoverflow.com/questions/153234/how-deep-are-your-unit-tests/I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence (I suspect this level of confidence is high compared to industry standards, but that could just be hubris). If I don’t typically make a kind of mistake (like setting the wrong variables in a constructor), I don’t test for it. I do tend to make sense of test errors, so I’m extra careful when I have logic with complicated conditionals. When coding on a team, I modify my strategy to carefully test code that we, collectively, tend to get wrong. Different people will have different testing strategies based on this philosophy, but that seems reasonable to me given the immature state of understanding of how tests can best fit into the inner loop of coding. Ten or twenty years from now we’ll likely have a more universal theory of which tests to write, which tests not to write, and how to tell the difference. In the meantime, experimentation seems in order.Kent Beck - Extreme Programming ExplainedWhat kind of code would Kent Beck avoid unit testing?It is impossible to test absolutely everything, without the tests being as complicated and error-prone as the code. It is suicide to test nothing (in this sense of isolated, automatic tests). So, of all the things you can imagine testing, what should you test? You should test things that might break. If code is so simple that it can't possibly break, and you measure that the code in question doesn't actually break in practice, then you shouldn't write a test for it... Testing is a bet. The bet pays off when your expectations are violated [when a test that you expect to pass fails, or when a test that you expect to fail passes]... So, if you could, you would only write those tests that pay off. Since you can't know which tests would pay off (if you did, then you would already know and you wouldn't be learning anything), you write tests that might pay off. As you test, you reflect on which kinds of tests tend to pay off and which don't, and you write more of the ones that do pay off, and fewer of the ones that don't.테스팅 전략의 중요성좋은 테스트의 요소 중 여러 개를 동시에 만족하기는 어려움 (서로 상충됨)예) 테스트 단위가 작으면장점: 실행속도가 빠르고 엣지 케이스 검증이 쉬움단점: 작은 단위의 변화에도 깨짐, 모의 객체 사용이 늘어 버그 검출이 어려워짐프로젝트의 특성에 따라 더 중요한 가치를 판단해서 전략을 세워야 함테스팅 ROI를 고려해서 가장 효율적인 전략을 세우는 것이 필요프런트엔드 테스트프런트엔드의 구성 요소시각적(visual) 표현화면에 표시되는 비주얼 요소를 디자인 요구사항에 맞게 구현레이아웃 / 색상 / 폰트 / 이미지 / 애니메이션 등주로 HTML(DOM) / CSS 에 의해 결정사용자 입력 처리사용자에 의한 마우스/키보드 입력 등을 요구사항에 맞게 처리주로 DOM에 바인딩된 이벤트 핸들러에 의해 처리어플리케이션 상태 관리사용자 입력 등에 의해 변경되는 어플리케이션의 상태를 관리Routing / 팝업 표시-숨김 / 읽기-편집 모드 변경 / 에러 메시지 표시주로 순수 자바스크립트에 의해 처리서버와의 통신REST API / Socket 등으로 서버와 통신하며 어플리케이션 상태를 동기화주로 브라우저 API 혹은 라이브러리를 사용해서 비동기 로직을 수행프런트엔드 요소별 테스트 전략시각적 표현실제 화면을 픽셀 단위로 테스트 -> 눈으로 직접 확인 or 자동 스크린샷 테스트HTML 구조를 테스트 -> HTML 구조를 직접 입력 or 스냅샷 테스트특정 DOM 요소의 상태만 테스트 (버튼의 상태 / 텍스트) -> 시각적 테스트 아님사용자 입력 처리자바스크립트 API를 사용한 이벤트 시뮬레이션라이브러리(jquery, React)를 이용한 이벤트 시뮬레이션E2E 도구를 이용한 이벤트 시뮬레이션서버와의 통신실제 API 서버를 이용 -> 통합/E2E 테스트Ajax 통신 모듈을 Mocking / 가상 API 서버를 구축 -> 단위/통합 테스트서비스 레이어를 분리해서 Mocking - 단위/통합 테스트어플리케이션 상태 관리어플리케이션의 상태를 관리하는 레이어만 분리해서 테스트 -> 단위 테스트상태와 바인딩 된 DOM 요소의 상태를 테스트 -> 통합 테스트프런트엔드 테스트 전략: 고려할 점어플리케이션의 종류비주얼 요소가 중요한가 (예: 차트)모든 브라우저에서 테스트해야 하는가 (예: 에디터)어플리케이션의 규모 및 복잡성간단한 라이브러리 (예: 데이트 피커)복잡한 라이브러리 (예: 그리드)복잡한 웹 서비스 (예: 두레이, 토스트 파일)팀 구성별도의 QA팀이 있는가서버-클라이언트를 모두 통제할 수 있는가이벤트 시뮬레이션 방식순수 자바스크립트createEvent() 혹은 CustomEvent 생성자를 이용해서 이벤트를 생성dispatchEvent() 를 이용해 이벤트 발생시뮬레이션이 어려운 이벤트 (mousemove, mouseover 등) 는 테스트 불가라이브러리/프레임워크jQuery 등의 DOM 라이브러리 사용시 trigger() 등을 통해 간단하게 시뮬레이션 가능React는 자체적인 이벤트 시스템을 갖기 때문에 React 에서 제공하는 이벤트 시뮬레이션 함수를 사용Angular, Vue 등도 이벤트 시뮬레이션 도구 제공시뮬레이션이 어려운 이벤트 (mousemove, mouseover 등) 는 테스트 불가E2E (Selenium Webdriver)실제 브라우저의 행위를 시뮬레이션커스텀 이벤트를 발생시키는 것보다 더 실제 환경에 가깝게 테스트 가능실습 : UI 테스트 (counter)예제 설명npm 스크립트npm run app:uiCounter 브라우저 접속http://localhost:1234소스코드src/uiCounter/counter.jssrc/uiCounter/index.jsuiCounter.htmlFixture 셋팅 및 초기화테스트를 진행할 때마다 document.body 에 필요한 HTML 구조를 준비시켜 주어야 함테스트 코드let $container; beforeEach(() => { $container = $('<div>'); $container.appendTo(document.body); createUICounter($container, { initVal: 10, min: 8, max: 12 }); }); afterEach(() => { document.body.innerHTML = ''; }); 테스트 전략1: HTML 구조 테스트전략HTML을 문자열로 직접 비교diff가 용이하도록 HTML 구조를 만들어주는 라이브러리 사용https://github.com/rayrutjes/diffable-html테스트 코드test/uiCounter/html.spec.jsimport prettyHTML from 'diffable-html'; // .. 초기화 it('생성시 버튼과 초기값을 렌더링한다.', () => { expect(prettyHTML($container.html())).toBe(prettyHTML( <button type="button" class="btn btn-secondary btn-dec">-</button> <span class="value">10</span> <button type="button" class="btn btn-primary btn-inc">+</button> )); }); it('+ 버튼 클릭시 1 증가한다.', () => { $container.find('.btn-inc').click(); expect(prettyHTML($container.html())).toBe(prettyHTML( <button type="button" class="btn btn-secondary btn-dec">-</button> <span class="value">11</span> <button type="button" class="btn btn-primary btn-inc">+</button> )); }); it('Max값인 경우 + 버튼이 disabled 상태가 되며 클릭해도 증가하지 않는다.', () => { $container.find('.btn-inc').click(); $container.find('.btn-inc').click(); $container.find('.btn-inc').click(); expect(prettyHTML($container.html())).toBe(prettyHTML( <button type="button" class="btn btn-secondary btn-dec">-</button> <span class="value">12</span> <button type="button" disabled class="btn btn-primary btn-inc">+</button> )); }); 테스트 전략2: 스냅샷 테스트전략현재의 HTML 구조를 그대로 파일로 저장한 후, 변경될 때마다 테스트 실패실제 결과는 파일 내부를 직접 확인해 보아야 함HTML을 직접 비교하는 것보다 간편함회귀 테스트의 역할테스트 코드test/uiCounter/snapshot.spec.jsit('생성시 버튼과 초기값을 렌더링한다.', () => { expect($container.html()).toMatchSnapshot(); }); it('+ 버튼 클릭시 1 증가한다.', () => { $el.find('.btn-inc').click(); expect($container.html()).toMatchSnapshot(); }); it('Max값인 경우 + 버튼이 disabled 상태가 되며 클릭해도 증가하지 않는다.', () => { $el.find('.btn-inc').click(); $el.find('.btn-inc').click(); $el.find('.btn-inc').click(); expect($container.html()).toMatchSnapshot(); }); 스냅샷 파일test/uiCounter/snapshots/snapshot.spec.js.snapexports[+ 버튼 클릭시 1 증가한다. 1] = <button type="button" class="btn btn-secondary btn-dec"

</button> <span class="value"> 11 </span> <button type="button" class="btn btn-primary btn-inc"

</button> ; 전략 1,2에 대한 평가세부 구현 테스트실제 UI에 대한 결과물은 픽셀로 표현되는 화면이지 HTML이 아님HTML은 세부 구현 방식에 대한 내용HTML은 동일해도 CSS에 따라 결과물(픽셀)이 완전히 달라질 수 있음HTML이 변경되어도 결과물(픽셀)은 동일할 수 있음테스트 코드(HTML 구조)를 보고 의도(실제 화면)을 파악하기 어려움스냅샷 테스트테스트 코드를 보고 의도를 파악하기 어려움테스트 의도를 코드로 입력하지 않음 -> 회귀 테스트의 용도로만 사용스냅샷 데이터의 Diff를 보아도 의도를 파악하기 어려울 때가 있음습관적인 업데이트 -> 테스트의 신뢰성 감소시각적 테스트시각적 테스트의 어려움시각적 요소에 대한 테스트는 자동화하기가 어려움스크린샷을 직접 찍어 비교하는 방식이 가장 테스트의 목적에 부합함원하는 아웃풋을 미리 만들어내기가 어렵기 때문에 "회귀(Regression)" 테스트만 가능디자인 시안이 케이스별로 잘 정리되어 있다면 기대값으로 사용 가능하지만, 브라우저 여백, 이미지 사이즈 등 기술적으로 극복해야 할 부분이 많음또한, 브라우저나 OS에 따라 렌더링 방식이 다르기 때문에 기술적 한계가 있음동일한 이미지인데 픽셀이 미세하게 다른 경우가 많음변경된 부분을 정확히 감지하고, 의도된 변경인지 아닌지 파악하기가 어려움변경점과 이력 관리를 위한 별도의 도구 필요스토리북(Storybook)애플리케이션에서 분리된 별도의 환경에서 UI만 개발할 수 있도록 도와주는 도구시각적 표현에 대한 개발만 빠르게 진행할 수 있음컴포넌트 방식의 개발에 잘 어울림 (React, Vue, Angular)컴포넌트 갤러리로 활용디자이너/기획자와의 커뮤니케이션 도구로써 유용함Airbnb DatesWix Style스토리북을 사용하면 시각적 테스트를 수동으로 진행할 때 더 효율적임시각적 회귀(regression) 테스트최근 기술적 한계를 극복한 다양한 테스트 도구가 출시되어 발전하는 중AI 등을 활용해서 의미있는 차이만 감지대규모 스냅샷 파일의 이력 관리, 이미지 직접 비교, 스냅샷 갱신 등의 관리를 도와줌스토리북 / Crypess / 셀레니움 등의 도구와 연동해서 사용할 수 있음PercyApplitoolsChromatic시각적 요소와 기능적 요소 분리하기시각적 요소와 기능을 같이 테스트하는 것은 테스트의 복잡도를 증가시킴시각적 요소를 스토리북과 같은 도구로 분리하고, 기능과 상태 변경만 테스트하는 것이 효율적임실제 UI의 "컨텐츠(상태)" 만을 테스트 (시각적 요소 제외)버튼의 존재 유무 / 버튼의 상태 / 화면상의 텍스트 등HTML 구조와의 종속성을 최소화하기기능만을 위한 별도의 HTML 속성, 클래스 등을 사용CSS 셀렉터 사용 시 태그, 자식 선택자 등을 지양dom-testing-libraryhttps://github.com/kentcdodds/dom-testing-library"사용자의 관점에 가깝게 테스트 할 수록 개발자에게 더 많은 자신감을 준다."The more your tests resemble the way your software is used, the more confidence they can give you.특징CSS 셀렉터를 지양하고 사용자가 볼 수 있는 텍스트 위주의 셀렉터를 제공getByTextgetByLabelTextgetByAltTextgetByTitlegetByValue...텍스트를 사용할 수 없는 경우에는 data-test-id 속성을 사용하도록 권장getByTestId테스트 코드가 DOM 구조의 변경에 영향을 받지 않도록 하기 위함이벤트를 시뮬레이션할 수 있는 함수를 제공fireEventDOM이 변경되거나 특정 단언(Assertion)이 성공할 때까지 기다릴 수 있는 함수를 제공waitwaitForElementwaitForDomChange비동기 로직을 테스트할 때 사용예제 코드it('examples of some things', async () => { const famousWomanInHistory = 'Ada Lovelace' const container = getExampleDOM() // Label 텍스트로 검색 (없으면 에러 발생) const input = getByLabelText(container, 'Username') input.value = famousWomanInHistory // 실제 텍스트로 검색 (없으면 에러 발생) getByText(container, 'Print Username').click() await wait(() => // [data-testid="printed-username"]인 요소 검색 -> 없으면 계속 시도 (timeout 까지) expect(queryByTestId(container, 'printed-username')).toBeTruthy() ) }); jest-domhttps://github.com/testing-library/jest-domDOM과 관련된 다양한 형식의 매처(matcher)를 추가사용예import '@testing-library/jest-dom/extend-expect'; it('test', () => { // visibility 검사 expect(getByText(container, '+')).toBeVisible(); expect(getByText(container, '-')).toBeVisible(); // DOM 문서(body)에 포함되었는지 여부를 확인 expect(getByText(container, '10')).toBeInTheDocument(); // 특정 클래스를 갖는지 검사 expect(getByText(container, '+')).toHaveClass('.btn-inc'); expect(getByText(container, '-')).toHaveClass('.btn-dec'); // Disabled 상태 검사 expect(getByText(container, '+')).toBeDisabled(); expect(getByText(container, '-')).toBeDisabled(); // 특정 텍스트를 포함하는지를 검사 expect(getByTestId(container, 'value')).toHaveTextContent('10'); }); 실습: counter 테스트목표jquery를 사용하지 않고 테스트한다.dom-testing-library와 jest-dom를 사용해서 UI를 테스트하는 방법을 익힌다.다음의 테스팅 전략을 수행한다.시각적 요소는 테스트하지 않는다.사용자의 관점에서 보이는 텍스트 위주로 테스트한다.fireEvent 함수를 이용해 이벤트를 발생시킨다.진행 방법스크립트 : npm run test:uiCounter테스트 파일 : test/uiCounter/dom.spec.js결과 평가전략1,2와 비교해서 장/단점은 무엇인가Mocking모듈 Mockingimport {createCounter} from '../../src/counter'; jest.mock('../../src/counter'); it('생성시 주어진 옵션으로 counter를 생성한다.', () => { createCounter.mockImplementation(() => ({ val: () => 10, isMin: () => false, isMax: () => false })); const options = { initVal: 10, min: 8, max: 12 }; createUICounter(container, options); expect(createCounter).toHaveBeenCalledWith(options); }); Mock(Spy) 함수 생성test('jest.fn()', () => { const mockFn = jest.fn(); mockFn(1, 'one'); mockFn(2, 'two'); expect(mockFn).toHaveBeenCalled(); expect(mockFn).toHaveBeenCalledTimes(2); expect(mockFn).toHaveBeenCalledWith(1, 'one'); expect(mockFn).toHaveBeenCalledWith(2, 'two'); expect(mockFn.mock.calls[0]).toEqual([1, 'one']); expect(mockFn.mock.calls[1]).toEqual([2, 'two']); }); 추가학습 : counter 단위 테스트(Mock)Mock을 이용해 UI만 테스트기존 createCount 테스트와 겹치지 않게 View만 테스트하기createCount를 Mocking 해서 테스트테스트 코드test/uiCounter/unit.spec.js단점테스트 코드가 더 장황해짐세부 구현에 대한 의존도가 높아짐리팩토링할 때 테스트가 실패할 확률이 높아짐실습 : 서버 API 통신 (Counter)예제 설명구성카운터 로직을 서버에서 처리 (parcel 번들러에서 서버 로직 구현)Ajax 요청을 위해 axios 라이브러리 사용기존 UI 카운터는 서버의 데이터를 받아서 렌더링만 하도록 변경실행 방법npm run app:serverCounterAjax Mocking이유실제 API 서버를 사용해서 테스트하면 데이터를 조작하기가 어려움Ajax 요청을 Mocking하면 API 명세만 갖고도 개발/테스트가 가능방법Ajax를 호출하는 서비스 레이어 모듈을 MockingAjax를 호출하는 라이브러리와 호환되는 Mock 라이브러리를 사용https://github.com/ctimmerm/axios-mock-adapterhttps://github.com/jakerella/jquery-mockjaxhttps://github.com/wheresrhys/fetch-mock별도의 가상 API 서버를 구현https://github.com/ijpiantanida/talkbackaxios-mock-adaptermock 객체 생성import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; const mockAxios = new MockAdapter(axios); 기본 요청 MockmockAxios.onGet('/users').reply(200, { users: [ { id: 1, name: 'John Smith' } ] }); 특정 파라미터 요청 MockmockAxios.onGet('/users', { params: { searchText: 'John' } }).reply(200, { users: [ { id: 1, name: 'John Smith' } ] }); 요청 이력 확인expect(mockAxios.history.get.length).toBe(1); expect(mockAxios.history.get[0].data).toEqual({ page: 1 }); 비동기 테스트done 파라미터 사용it('서버 데이터 확인', (done) => { fetchUserData(1).then(data => { expect(data).toEqual('Mark'); done(); }); }); Promise 반환it('서버 데이터 확인', () => { return fetchUserData(1).then(data => { expect(data).toEqual('Mark'); }); }); async/await 사용it('서버 데이터 확인', async () => { const data = await fetchUserData(1); expect(data).toEqual('Mark'); }); dom-testing-library의 비동기 테스트waitForDomChangecontainer DOM이 변경될 때까지 대기it('DOM 변경 확인', async () => { const container = document.createElement('div'); createBoard(container); await waitForDomChange({container}); expect(getByText('container', 'Board')).toBeVisible(); }); wait콜백 함수 내의 expect 문이 성공할 때까지 대기it('DOM 변경 확인', async () => { const container = document.createElement('div'); createBoard(container); await wait(() => { expect(getByText('container', 'Board')).toBeVisible(); }); }); 그 외waitForElementwaitForElementToBeRemoved실습 내용목표서버 API와의 상호작용을 테스트하는 방법을 익힌다.async/await를 이용해서 비동기 로직을 테스트하는 방법을 익힌다.axios-mock-adapter 를 이용해 Ajax 요청을 Mocking 하는 방법을 익힌다.진행 방법테스트 실행npm run test:serverCounter테스트 파일 :test/serverCounter.spec.js소스 파일src/serverCounter/index.jssrc/serverCounter/counter.js서버/번들러 파일serverCounterBundle.js커버리지 확인npm run test:coverage E2E 테스트Selenium WebdriverSelenium RC(Remote Control)Selemium1.0자바스크립트를 브라우저 내부에 삽입해서 제어하는 방식자바스크립트의 샌드박스를 벗어나지 못함모든 종류의 브라우저를 완벽하게 지원하지 않음더이상 지원되지 않음Selenium WebDriverSelemium 2.0브라우저 외부에서 제어하는 방식네이티브 수준에서 브라우저를 직접 제어WebDriver 명세를 따르는 각 브라우저별 웹드라이버를 설치해서 사용대부분의 브라우저 및 실행환경을 지원FirefoxDriver / ChromeDriver / InternetExplorerDriver / SafariDriver / AppiumWebDriver 명세https://www.w3.org/TR/webdriver/서버/클라인언트 구조에서 HTTP를 이용해 브라우저에게 명령을 내릴 수 있는 API를 정의Selenium Grid허브 역할을 하는 머신이 노드 역할을 하는 모든 머신에게 테스트를 지시한 후 결과를 모아서 반환테스트를 다양한 환경의 머신에서 동시에 수행할 수 있는 환경을 제공활용QA 개발자 모두가 사용WebDriver 클라이언트의 언어는 어떤 언어든 사용 가능Java / C# / Ruby / Python / JavascriptSelenium Webdriver 라이브러리Protractorhttp://www.protractortest.org프로젝트 활성화 & 문서화가 가장 잘 되어 있음Angular 프로젝트에서만 사용WDhttp://admc.io/wdJsonWireProtocol 를 자바스크립트로 호출할 수 있는 API만을 제공테스트 러너 / Assertion 등의 지원 없음 : 테스트 환경 별도 구축 필요플러그인 지원 없음 / 문서는 기본적인 수준Repl 지원Promise(Q) 기반 / 체이닝 형식의 API제너레이터 기반의 라이브러리도 제공 (Yiewd)browser .get("<http://www.google.com>") .elementById('q') .sendKeys('webdriver') .elementById('btnG') .click() seleniumn-webdriverjshttps://github.com/SeleniumHQ/selenium/wiki/WebDriverJsSelenium webdriver의 정식 Node.js 구현체테스트 러너 / Assertion 등의 지원 없음 : 테스트 환경 별도 구축 필요플러그인 지원 없음 / 문서는 기분적인 수준API 가 복잡함driver.get('<http://www.google.com>'); driver.findElement(webdriver.By.id('q')).sendKeys('webdriver'); driver.findElement(webdriver.By.id('btnG')).click(); NightWatchhttp://nightwatchjs.orgMocha 기반의 테스트 러너 지원Assertion 라이브러리 선택 가능커뮤니티 활성화 & 문서화 잘 되어 있음SauceLabs / BrowserStack 과 같은 외부 서비스 지원browser .url('<http://www.google.com>') .setValue('#q', 'webdriver') .click('#btnG'); WebdriverIOhttp://webdriver.io테스트 러너 지원Assertion 라이브러리 선택 가능 (Jasmine 플러그인 지원)커뮤니티 활성화 & 문서화 잘 되어 있음 (NightWatch 보다 더)SauceLabs / BrowserStack 과 같은 외부 서비스 지원Static 웹서버 + Webpack 통합 지원Visual Regression 테스트 지원Jenkins 통합 지원Repl 인터페이스 지원확장성 좋음client .url('<http://google.com>') .setValue('#q', 'webdriver') .click('#btnG') 단위/통합 테스트 vs E2E 테스트단위/통합 테스트의 단점실제 사용자의 환경에서 발생하는 버그를 검출하기 어려움각 모듈간의 연결에서 발생하는 버그를 검출하기 어려움Jest 등의 Node 환경에서 테스트하는 경우 실제 화면을 볼 수가 없음E2E 테스트의 단점속도가 느림테스트 코드를 작성하기가 번거로움실행 환경에 대한 통제가 쉽지 않아 안정성이 떨어짐해결책사용자의 관점에서 테스트를 하면서도 빠르고 사용하기 쉽고 안정적인 환경이 필요최근 E2E 테스트의 단점을 극복한 다양한 도구들이 출시최신 E2E 도구들Cypresshttps://github.com/cypress-io/cypress브라우저를 다룰 수 있는 별도의 드라이버를 만들어서 사용E2E 뿐만 아니라, 통합, 단위 테스트까지 사용 가능GUI 도구를 지원. 스펙 관리 및 디버깅이 편리함.브라우저 내부에서 테스트 (현재 크롬만 지원)브라우저 지원 로드맵 : https://github.com/cypress-io/cypress/issues/310주요 개념 : https://docs.cypress.io/guides/overview/key-differences.htmlDashBoard모든 테스트 과정과 결과를 저장하고, 한 눈에 분석/관리할 수 있는 서비스 (유료)test cafehttps://devexpress.github.io/testcafe/프록시 서버를 통해 스크립트를 페이지에 주입해서 사용모든 브라우저에서 사용 가능 (모바일, 클라우드 브라우저 포함)Test Cafe StudioGUI를 통해 손쉽게 테스트 관리 및 작성 (유료)Selenium vs CypressSeleniumCypress사용자QA / 개발자프론트엔드 개발자사용 언어대부분의 언어자바스크립트테스트 프레임워크제한 없음Mocha지원 브라우저대부분의 브라우저크롬 (추가 예정)동작 방식브라우저 외부브라우저 내부속도느림빠름서버 목킹별도의 도구 사용내장디버깅어려움쉬움실습: Cypress 설치 및 사용설치npm을 이용한 설치npm install cypress --save-dev npm 스크립트 등록package.json**{"scripts" : {"cypress:open" : "cypress open"}}실행**npm run cypress:open 결과 확인실행 화면액션 로그실습: E2E 테스트 (cypress)cypress 기본기본 예제describe('ui-counter', () => { beforeEach(() => { // 페이지 접속 cy.visit('<http://localhost:1234/>') }) it('+ 버튼 클릭시 1 증가한다.', () => { // btn-inc 클래스를 가진 요소를 클릭 cy.get('.btn-inc').click(); // value 클래스를 가진 요소의 텍스트가 10 cy.get('.value').should('have.text', '10'); }); 선택자// 클래스가 'btn-inc' 인 엘리먼트 cy.get('.btn-inc'); // '+' 텍스트를 포함하는 엘리먼트 cy.contains('+'); shouldAssersion API: https://docs.cypress.io/guides/references/assertions.html// 보임 cy.get('.login').should('be.visible'); // 'form-horizontal' 클래스가 있음 cy.get('form').should('have.class', 'form-horizontal'); // value가 'Jane'이 아님 cy.get('input').should('not.have.value', 'Jane'); // 텍스트가 '10' cy.get('.value').should('have.text', '10'); // disabled 속성이 있음(true) cy.get('.btn-inc').should('have.attr', 'disabled'); cypress-testing-librarydom-testing-library의 선택자들을 cypress 에서도 사용할 수 있도록 cy 객체를 확장사용 방법import 'cypress-testing-library/add-commands'; 예제 코드cy.getAllByText('Jackie Chan').click() cy.queryByText('Button Text').should('exist') cy.queryByText('Non-existing Button Text').should('not.exist') cy.queryByLabelText('Label text', {timeout: 7000}).should('exist') cy.get('form').within(() => { cy.getByText('Button Text').should('exist') }) cy.get('form').then(subject => { cy.getByText('Button Text', {container: subject}).should('exist') }) 실습1: ui-counter목표cypress로 UI의 상태를 테스트하는 방법을 익힌다.기존에 작성했던 ui-counter의 테스트와 동일한 명세를 cypress로 작성한다.Jest로 작성한 테스트와의 장단점을 비교해본다.진행 방법cypress 실행 :npm run cypress:open서버 실행npm run app:uiCounter테스트 파일cypress/integration/uiCounter.spec.js서버 목킹// 목킹 활성화 cy.server(); // /counter (GET) 요청에 대한 응답값 지정 cy.route('/counter', { value: 10, isMin: false, isMax: false }); // counter/inc (PUT) 요청에 대한 응답값 지정 cy.route('/counter/inc', { value: 10, isMin: false, isMax: false }); 실습2: server-counter목표cypress로 서버 API를 목킹한 후 UI의 상태를 테스트하는 방법을 익힌다.기존에 작성했던 server-counter의 테스트와 동일한 명세를 cypress로 작성한다.Jest로 작성한 테스트와의 장단점을 비교해본다.진행 방법cypress 실행 :npm run cypress:open서버 실행npm run app:serverCounter테스트 파일cypress/integration/serverCounter.spec.js정리프런트엔드 테스트 전략시각적 요소 vs 기능적 요소시각적 요소를 HTML이나 CSS를 이용해 테스트하는 것은 지양한다.스토리북 등의 도구를 사용하여 시각적 요소의 다양한 상태를 테스트하기 쉽게 관리한다.이미지 비교 방식의 테스트 도구와 스토리북을 결합하면 시각적 테스트를 자동화할 수 있다.기능적 요소를 테스트할 때는 최대한 HTML 구조의 변화에 영향을 받지 않도록 한다.테스트 단위핵심 알고리즘을 담고 있는 모듈이 아니면 단위 테스트를 지양한다.내부 모듈간의 의존성을 Mocking 하는 것은 지양하고, 통합 테스트 위주로 작성한다.Cypress와 같은 최신 E2E 도구는 더 나은 프런트엔드 테스트 환경을 제공해 준다.QnAHappy Testing!!