PMPV

assert equal은 어디에서 나온 놈일까? 본문

Ruby on Rails/ruby koan

assert equal은 어디에서 나온 놈일까?

playinys 2018. 1. 25. 23:59
반응형

오늘은 어피치 초롱


ruby koan에서 assert_equal은 어떻게 써야하는 놈일까?

어디에서 나왔을까?

assert_raise는 또 뭐고, 어떤 놈들이 있을까?


1.

오늘의 질문
26번 문제를 물어보는 줄 알고 열심히 26번까지 풀었는데 26 라인을 얘기한거였다.
고맙다 어피치야

26번째 라인은 다음과 같다.

def test_accessing_hashes_with_fetch
hash = { :one => "uno" }
assert_equal "uno", hash.fetch(:one)
assert_raise(__) do
hash.fetch(:doesnt_exist)
end

# THINK ABOUT IT:
#
# Why might you want to use #fetch instead of #[] when accessing hash keys?
end
문제의 assert_raise

지난 포스팅에서 assert_equal은 콘마를 기준으로 좌항과 우항을 대비해서 풀면 된다고 알려줬는데, 난데없이 등장한 assert_raise 때문에 당황한 것 같다.
이번엔 어떻게 설명해줄까 하다가 갑자기 궁금해졌다. 이건 어디서 나온거지?
그래서 ruby koan을 뒤져봤다. 역시나 neo.rb에 아래와 같은 내용이 있었다.

module Assertions
FailedAssertionError = Class.new(StandardError)

def flunk(msg)
raise FailedAssertionError, msg
end

def assert(condition, msg=nil)
msg ||= "Failed assertion."
flunk(msg) unless condition
true
end

def assert_equal(expected, actual, msg=nil)
msg ||= "Expected #{expected.inspect} to equal #{actual.inspect}"
assert(expected == actual, msg)
end

def assert_not_equal(expected, actual, msg=nil)
msg ||= "Expected #{expected.inspect} to not equal #{actual.inspect}"
assert(expected != actual, msg)
end

def assert_nil(actual, msg=nil)
msg ||= "Expected #{actual.inspect} to be nil"
assert(nil == actual, msg)
end

def assert_not_nil(actual, msg=nil)
msg ||= "Expected #{actual.inspect} to not be nil"
assert(nil != actual, msg)
end

def assert_match(pattern, actual, msg=nil)
msg ||= "Expected #{actual.inspect} to match #{pattern.inspect}"
assert pattern =~ actual, msg
end

def assert_raise(exception)
begin
yield
rescue Exception => ex
expected = ex.is_a?(exception)
assert(expected, "Exception #{exception.inspect} expected, but #{ex.inspect} was raised")
return ex
end
flunk "Exception #{exception.inspect} expected, but nothing raised"
end

def assert_nothing_raised
begin
yield
rescue Exception => ex
flunk "Expected nothing to be raised, but exception #{exception.inspect} was raised"
end
end
end

우리가 쓰던 assert equal은 여기서 나왔다. 여기를 보면 assert raise도 알 수 있을 것 같다.
하나하나 뜯어보자.

def assert_equal(expected, actual, msg=nil)
msg ||= "Expected #{expected.inspect} to equal #{actual.inspect}"
assert(expected == actual, msg)
end


우리가 가장 많이 쓰던 assert_equal

이 줄을 해석하기 위해 생소한 개념들부터 찾아보자. 우선 저 짝대기 두 개 (double fipe)
루비에서 두 개의 파이프는 OR을 나타내는 연산자이다.
여기서 뒤에 = 이 붙게 되면, 조건부 할당 연산자가 된다.
해당 변수에 할당된 값이 없을 때, 새로운 값을 할당하라는 뜻이다.

또, #{}은 문자열 안에 변수를 삽입하는 방식이다. 레일즈 웹 프로그래밍에서 URL 매칭에도 많이 쓰인다.

inspect 메서드는 따로 정리해둔 포스팅을 참고하자.

그렇다면 이 소스를 읽어보자.

assert_equal은 세 가지 인자를 받는다. expected, actual, msg=nil

msg가 nil이라면, "Expected #{expected의 값} to equal {actual의 값}"를 출력한다.

assert (expected 와 actual는 같다, 위에서 할당된 msg)

assert는 뭐지?

module Assertions
FailedAssertionError = Class.new(StandardError)

def flunk(msg)
raise FailedAssertionError, msg
end

def assert(condition, msg=nil)
msg ||= "Failed assertion."
flunk(msg) unless condition
true
end

assert_equal 바로 윗 줄이다.
Assertions 모듈의 구조를 보자. 모듈 첫 줄에 FailedAssertionError = Class.new(StandardError) 라는 정의가 있다.
StandardError는 프로그램의 표준 스트림이다.
일반적으로 유닉스 환경에서 실행되는 프로그램은 프로그램이 실행될 때 자동으로 3가지의 스트림 채널이 열린다. 이 3가지는 입력을 위한 스트림인 STDIN, 출력을 위한 STDOUT, 오류 메세지 출력을 위한 STDERR이고, 마지막의 STDERR이 standarderror와 같은 놈이다.
설명이 복잡한데, 자세한 개념은 위키피디아를 참조하자.


쉽게 생각하자면, 우리가 소스코드를 작성하면서 마주치는 에러들, 예를 들면 NoMethodError, TypeError, ArgumentError 등이 있다. 이 에러 메세지는 수시로 나오는 에러이고, 이 에러가 발생했을 때의 처리를 항상 똑같이 하기 위해 에러를 하나로 묶었다고 생각하면 쉽겠다. 그 묶음이 standard error이다. 모듈의 첫 번째 줄은 이 프로그램에서는 FailedAssertionError 이라는 에러가 자주 발생할 것 같으니 스탠다드 에러의 꼭지를 하나 추가해서 여기서 따로 정의한다고 생각하자.


그럼 그 다음 액션부터는 발생 예상되는 에러의 처리를 정의한 것이다. 첫 줄의 flunk는 FailedAssertionError를 발생시켜주고, 인자로 받는 놈을 메세지로 전달해준다. irb를 켜서 저 소스를 옮겨적으면 이렇게 나온다.



 :001 > FailedAssertionError = Class.new(StandardError)

  => FailedAssertionError 

 :002 > def flunk(msg)

 :003?>   raise FailedAssertionError, msg

 :004?>   end

  => :flunk 

 :005 > flunk "hello"

 FailedAssertionError: hello

from (irb):3:in `flunk'

from (irb):5

from /Users/p/.rvm/rubies/ruby-2.3.3/bin/irb:11:in `<main>'



flunk 함수를 정의해주고 인자로 "hello"를 전달해주었다. 그러자 오류 메세지에 FailedAssertionError: hello가 뜨는 것을 볼 수 있다.


이런 식으로 assert 함수도 따로 정의해준다. assert 함수는 하나의 인자를 받고, 동시에 메세지를 받는다.

assert_equal 함수에서, 마지막에 


assert (expected 와 actual는 같다, 위에서 할당된 msg)


이런 내용이 있었다. 여기선 expected == actual의 값을 assert 함수에 인자로 전달한 모습이다.

만약 두 개의 인자가 같지 않다면, assert_equal은 false를 assert에 전달해주고, 메세지를 같이 전달해준다.

그럼 assert 함수는 flunk 함수를 불러오고, FailedAssertionError을 불러오게 된다.


복잡복잡


그럼 assert_equal에서, 


assert (expected 와 actual는 같다, 위에서 할당된 msg)


expected와 actual이 같다면? assert에 전달되는 값은 true일것이다.

assert 함수는 false가 아닌 true를 인자로 받을 때, 그대로 true를 반환시킨다.


다시 처음으로 돌아가보자.


  def assert_equal(expected, actual, msg=nil)
msg ||= "Expected #{expected.inspect} to equal #{actual.inspect}"
assert(expected == actual, msg)
end

assert_equal 함수는 expected와 actual이 같아야 한다.

두 값이 다르다면, false를 전달해주고 최종적으로 FailedAssertionError 에러 메세지를 뽑아낸다.

그럼 다른 함수는?


  def assert_not_equal(expected, actual, msg=nil)
msg ||= "Expected #{expected.inspect} to not equal #{actual.inspect}"
assert(expected != actual, msg)
end


assert_not_equal은 expected와 actual이 달라야 한다.


  def assert_nil(actual, msg=nil)
msg ||= "Expected #{actual.inspect} to be nil"
assert(nil == actual, msg)
end

def assert_not_nil(actual, msg=nil)
msg ||= "Expected #{actual.inspect} to not be nil"
assert(nil != actual, msg)
end

이 두 함수 역시 예상 가능하게, actual이 nil 값을 가지거나, 가지지 않아야 한다.


  def assert_raise(exception)
begin
yield
rescue Exception => ex
expected = ex.is_a?(exception)
assert(expected, "Exception #{exception.inspect} expected, but #{ex.inspect} was raised")
return ex
end
flunk "Exception #{exception.inspect} expected, but nothing raised"
end

assert_raise 함수는 좀 더 복잡한 모양새를 하고 있다. 차근차근 읽어보자.

인자로는 exception을 받는다.


이 부분은 예외 처리에 관한 내용이다. 따로 포스팅을 해야겠다.


쉽게 생각하자면, assert_raise는 다음의 블록이 불러오는 오류의 종류를 맞추는 함수라고 생각하면 되겠다.

다시 처음으로 돌아가서, assert_raise의 활용을 보자.


def test_accessing_hashes_with_fetch
hash = { :one => "uno" }
assert_equal "uno", hash.fetch(:one)
assert_raise(__) do
hash.fetch(:doesnt_exist)
end

# THINK ABOUT IT:
#
# Why might you want to use #fetch instead of #[] when accessing hash keys?
end

네 번째 줄에서 assert_raise가 나온다. hash는 {:one => "uno"}라는 자료 구조를 가지고 있고, 여기에서 :doesnt_exist, 말 그대로 존재하지 않는 키로 접근했을 땐 어떻게 될까? 물론 오류가 나올 것이다. 어떤 오류?

Exception FillMeInError expected, but #<KeyError: key not found: :doesnt_exist> was raised

예외 ___가 예상되었지만, KeyError: key not found가 발생하였습니다.

다른 assert_raise의 활용도 같이 보자.

def my_global_method(a,b)
a + b
end

def test_calling_global_methods_with_wrong_number_of_arguments
exception = assert_raise(___) do
my_global_method
end
assert_match(/__ /, exception.message)

exception = assert_raise(___) do
my_global_method(1,2,3)
end
assert_match(/__ /, exception.message)
end

about_methods의 35번째 라인에 나오는 문제이다.

my_global_method는 두 개의 인자를 받아서 더해주는 간단한 함수인데, 이 두 가지 상황에서는 인자를 받지 않았거나, 잘못된 수의 인자를 받을 때를 가정하고 있다. 그럼 어떤 오류가 날까?

Exception FillMeInError expected, but #<ArgumentError: wrong number of arguments

(given 0, expected 2)> was raised

예외 ___가 예상되었지만, ArgumentError: wrong number of arguments가 발생하였습니다.

이렇게 오류의 이름을 알려주면, 공백에는 오류의 이름을 채워넣으면 된다.


알아보는 김에 assert_match까지 알아보자.

def assert_match(pattern, actual, msg=nil)
msg ||= "Expected #{actual.inspect} to match #{pattern.inspect}"
assert pattern =~ actual, msg
end

이 부분은 정규 표현식에 관한 내용이다. 이 부분도 따로 정리를 해서 포스팅을 하고, 지금은 활용 방법만 알아보자.

실제 문제 사례를 보자.


def test_calling_global_methods_with_wrong_number_of_arguments
exception = assert_raise(ArgumentError) do
my_global_method
end
assert_match(/__/, exception.message)

exception = assert_raise(ArgumentError) do
my_global_method(1,2,3)
end
assert_match(/__/, exception.message)
end

Expected "wrong number of arguments (given 0, expected 2)" to match /'___ '/

/__/ 다음엔 예외 메세지를 인자로 받는다.

여기선 __에 예외 메세지 문구를 적어주면 된다.

assert_match(/wrong number of arguments/, exception.message)

이렇게.


오늘의 포스팅은 여기까지. 아무래도 루비 코안 가이드 버전 투를 작성해야할듯

반응형

'Ruby on Rails > ruby koan' 카테고리의 다른 글

ruby koan 가이드 v.2  (0) 2018.02.06
inspect와 to_s는 뭐가 다른걸까?  (0) 2018.01.27
루비에서 object id는 왜 홀수일까?  (0) 2018.01.22
Comments