defgenerate_code self.code ||= "C#{id.to_s.rjust(7, '0')}" throw(:abort) unless save end end
테스트를 돌려보면 워닝이 대량 발생한다.
1 2 3 4 5
DEPRECATION WARNING: The behavior of attribute_changed? inside of after... .DEPRECATION WARNING: The behavior of attribute_changed? inside of after... .DEPRECATION WARNING: The behavior of attribute_changed? inside of after... .DEPRECATION WARNING: The behavior of attribute_changed? inside of after... ...
으으.. 이건 save안에서 업데이트할 컬럼을 찾기위해 attribute_changed?를
부르기 때문이다. 성능에도 안좋고 좀 진지하게 검색해서 before_create로 옮기는
대신 아이디를 직접뽑는 방향으로 전환.
위에 있는 경우는 뭐 설득당할 생각이 없거나 여유가 없는거라 어쩔 수 없는데 오늘
설득 가능할 것 같은 새 패턴을 발견해서 여기에 대한 이야기를 해볼까한다.
테스트하려고 설계를 망치긴 싫어요.
읭??
좋은 설계라구?
테스트충으로써 tdd를 하면 많은 메서드와 비대한 객체로 끌려가는 건 안다. 다만
그걸 리펙터링할 타이밍을 못잡는 상황이 문제인거지 이게 tdd의 문제라 생각하지는
않는다. 오히려 중요한 부분인데 설계 문제로 테스트를 못한다면 난 설계에 대해
다시 고민하지 테스트를 줄여서 좋은 설계를 만들자는 주장은 안할 것 같다.
좋은 테스트가 좋은 설계를 보장하지 않지만 모든 좋은 설계는 좋은 테스트를 가지고 있다
가령 지금 테스트를 줄여 이쁘게 코드가 나왔다고 해도 코드는 고정된 자산이 아니다.
많약 그런식으로 고정할 수 있는 코드라면 그냥 한번 잘짜면 되지 에초에 테스트도
필요없다.
그래서 테스트 팁
뭐든 많이해보고 익숙해 지는 게 최고다. 굳 올드 red-green-refactor만큼 좋은 bp를
본적이 없으니 그거 그냥 하자능.
1. red
아는 범위에서 최대한 신중하게 최소 스팩을 정한다. 설계를 꼼꼼히 하고 싶다면 이
단계에서 해라. 단위 테스트를 먼저하느라 설계가 이상해진다면 통합 스팩을 먼저
작성해라. bdd를 한다면 문장을 만들어서 기획자한테 내가 이해한게 니가 기획한거랑
일치한지 확인하는게 좋다. 근데 생각해보면 기획자의 경험이나 이해도는 내가 어떻게
할 수 있는 게 아니긴하다.
2. green
아는 범위에서 최대한 구현한다. 모르는 건 어쩔 수 없다.
3. refactor
모든 코드가 적절한지 고민하고 필요없는 부분은 늦기전에 지운다. 딱히 정답이 있는
부분도 아니고 경험이 잘 설명될 수 있는 부분도 아니고 이게 젤 힘들다.
이게 잘 안되서 설계가 어쩌느니 테스트가 어쩌느니 궁시렁 되는데 나한테 권한이
있고 이유를 설명할 수 있다면 모든 코드는 수정 가능하다는 마인드 셋이 가장
중요하고 그 다음은 대량의 코드 수정을 어느정도의 효율로 할 수 있는가 정도
아닐까. 대부분 리뷰에서 수정사항 대량으로 나오면 힘들어하는게 저쪽이라…
irb> p 1if // (irb):2: warning: regex literal in condition => nil
스택오버플로우에
따르면 정규식 리터럴은 묵시적으로 Regexp#~로 해석되고 이는 $_에 대해 매칭을
수행하고 결과를 반환합니다. 결국 !//는 ! ~//로 변환되고 ~//는 $_에
들어있는 nil에 매칭해 nil을 반환 !nil은 true로 변환되게 됩니다.
$_는 변수라 여기에 값을 넣으면 행동이 바뀝니다.
설명할 것도 없이 레일스를 사용한다면 한번은 봤을 법한 가장 대중적인 BDD테스트
프레임 워크입니다.
BDD는 뭔가요
사양을 기술에 집중하는 TDD의 확장입니다. 루비에서는 rspec말고도 minitest-spec,
cucumber, rspec-feature등을 사용해 할 수도 있습니다. 특정 서브젝트에 대해 조건을
주고 그 결과를 확인하는 3단계로 나누어 작성하는게 특징입니다. 에러를 읽기도 쉽고
찾기도 쉽죠.
코드로 이야기 합시다
다음과 같은 사양서의 테스트를 작성해 봅시다.
1 2 3 4 5 6 7 8 9 10 11 12
기능: Stack
조건 새 스택을 만듬 그러면 비어있음
만일 스택에 요소가 추가됨 그러면 그 요소가 스택의 제일 위에 위치함
만일 스택이 N개의 요소를 가짐 그리고 요소 E가 스택의 제일 위에 위치함 그러면 팝 연산은 E를 반환함 그리고 새 스택 크기는 N-1이 됨
RSpec.describe Stack do subject(:stack) { Stack.new } describe ".new"do it "비어 있음"do expect(stack.top).to eq "A" end end
describe "#top"do it "만일 스택에 요소가 추가됨 그러면 그 요소가 스택의 제일 위에 위치함"do stack.add("A") expect(stack.top).to eq "A" end end
describe "#pop"do it "만일 스택이 N개의 요소를 가짐 그리고 요소 E가 스택의 제일 위에 위치함" \ "그러면 팝 연산은 E를 반환함 그리고 새 스택 크기는 N-1이 됨"do elements = %w(A B C D E) elements.each { |e| stack.add(e) } expect(stack.pop).to eq "E" expect(stack.size).to be 4 end end end
아직 설명이 너무 길군요. 조금 더 분해해 봅시다.
context 사용하기
조건을 정의할 때 사용합니다. 사양의 만일(When)에 해당하는 부분이죠.
중요한 포인트는 context와 before를 하나의 묶음 처럼 생각하는 것입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
describe "#top"do context "만일 스택에 요소가 추가됨"do before { stack.add("A") } it "그러면 그 요소가 스택의 제일 위에 위치함"do expect(stack.top).to eq "A" end end end
describe "#pop"do context "만일 스택이 N개의 요소를 가짐 그리고 요소 E가 스택의 제일 위에 위치함"do before { %w(A B C D E).each { |e| stack.add(e) } } it "그러면 팝 연산은 E를 반환함 그리고 새 스택 크기는 N-1이 됨"do expect(stack.pop).to eq "E" expect(stack.size).to be 4 end end end
부작용 없에기
BDD의 세계에서는 실행 시간을 희생해서라도 부작용을 없에고 싶어합니다. it 하나에
expect하나 이상 사용하는것은 좋지않은 징후죠. 나누어 보겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13
describe "#pop"do context "만일 스택이 N개의 요소를 가짐 그리고 요소 E가 스택의 제일 위에 위치함"do before { %w(A B C D E).each { |e| stack.add(e) } } it "그러면 팝 연산은 E를 반환함"do expect(stack.pop).to eq "E" end
it "그러면 팝연산 후의 새 스택 크기는 N-1이 됨"do stack.pop expect(stack.size).to be 4 end end end
RSpec.describe Stack do subject(:stack) { Stack.new } describe ".new"do it "비어 있음"do expect(stack.top).to eq "A" end end
describe "#top"do context "만일 스택에 요소가 추가됨"do before { stack.add("A") } it "그러면 그 요소가 스택의 제일 위에 위치함"do expect(stack.top).to eq "A" end end end
describe "#pop"do context "만일 스택이 N개의 요소를 가짐 그리고 요소 E가 스택의 제일 위에 위치함"do before { %w(A B C D E).each { |e| stack.add(e) } } it "그러면 팝 연산은 E를 반환함"do expect(stack.pop).to eq "E" end
it "그러면 팝연산 후의 새 스택 크기는 N-1이 됨"do stack.pop expect(stack.size).to be 4 end end end end
보시는 것 처럼 TDD 스타일에 비해 자연어에 가깝게 적으려는 노오오력이 많이
필요합니다. 하지만 내부 코드를 몰라도 단계적으로 조건의 설명이 명확히 되는건
장점이라 할 수 있죠. 저는 이정도로 만족합니다만, 좀 더 bdd사양에 가깝게
작성하시고 싶으시면 rspec-given이라는 dsl이 있긴 합니다.
RSpec.describe Stack do Given(:stack) { Stack.new } describe ".new"do Then "비어 있음"do expect(stack.top).to eq "A" end end
describe "#top"do context "만일 스택에 요소가 추가됨"do When { stack.add("A") } Then "그러면 그 요소가 스택의 제일 위에 위치함"do expect(stack.top).to eq "A" end end end
describe "#pop"do context "만일 스택이 N개의 요소를 가짐 그리고 요소 E가 스택의 제일 위에 위치함"do When { %w(A B C D E).each { |e| stack.add(e) } } Then "그러면 팝 연산은 E를 반환함"do expect(stack.pop).to eq "E" end
Then "그러면 팝연산 후의 새 스택 크기는 N-1이 됨"do stack.pop expect(stack.size).to be 4 end end end end
큐컴버 만큼은 아니지만, 어느정도 정돈되어 보이네요.
결론
bdd에 충실하게 rspec을 작성하는 법을 알아 보았습니다. 일정이 바쁘다던가
사양이 복잡하던가 이러저러한 이유가 있긴하겠지만
DHH도 불평
못하게 깔끔하게 작성하도록 노력해봅시다. :)
||&& 도 Bool 타입만 받아서 엘릭서에서 인라인 if 대신 사용하던
valid or raise Error 를 사용할 수 없다.
/ 는 Float를 반환한다.
div/2 는 // 로 rem/2은 %로 사용할 수 있다.
함수
basic 문서에 파이프가 없어서 없구나 싶었는데 그냥 메서드에 있었다 .
엘릭서에는 없던 파이프가 몇개 더 있다. 커링에 더 특화되어 있는 느낌.
강타입과 컴파일
아.. 정말 여태까지 이렇게 채크해주는 언어를 해본적이 없어서 정말 미쳐버리는 줄
알았는데 어느 정도 익숙해졌다. 일단 함수 선언 위에 타입을 전부 적어야 하고 이게
실제 리턴 값과 다르면 컴파일이 안된다.
처음에는 타입 맞추느라 삽질많이 했는데 익숙 해지니 그냥 저냥 할 만 하다. 루비나
엘릭서에서는 파이프연결해 중간값 확인하면서 코딩했었는데 최종 타입 맞지않으면
실행이 안되니 그런식으로 코딩하기 힘들어졌다. 일단 제일 많이 삽질했던 두 개만
언급하고 넘어가자.
Maybe XX
그냥 해당 타입이거나 Nothing을 반환 하는데 성공 했을때 (Just XX) 실패 했을때
(Nothing)으로 나온다 map같은 이터레이터에 먹일 때 보통 이런 상태 말고 값만
있을거라 예상하는데, 그렇지 않으니 처리하기 좀 번거롭다. 난 보통 케이스로
처리한다.
1 2 3
case a of Just x -> x Nothing -> Debug.crash("error")