1. Getting Started

RubyMotion에서는 Bacon을 쓸 수 있습니다. Bacon은 유명한 RSpec 프레임워크의 축소판 클론으로 Christian Neukirchen님이 개발했습니다.

좀 더 구체적으로 말하자면, RubyMotion은iOS용으로 확장된 MacBacon이라는 버전을 사용하고 있습니다. MacBacon은 Eloy Duran님이 관리하고 있습니다.

1.1. Spec Files

Spec 파일에는 프로젝트의 테스트를 기술해야 합니다.

Spec 파일은 RubyMotion프로젝트의 spec 디렉터리의 밑에 작성합니다.

기본설정의 RubyMotion프로젝트는 spec/main_spec.rb 파일이 있고, 이 파일에는 애플리케이션이 윈도우를 가졌는지 확인하는 하나의 테스트가 적혀 있습니다.

1.2. Spec Helpers

Spec helpers는 클래스나 메소드를 정의해 테스팅 프레임워크를 확장하는 데 사용할 수 있습니다. Spec helpers는 스팩파일보다 먼저 컴파일되고 실행됩니다.

Spec helpers는 RubyMotion프로젝트의 spec/helpers 디렉터리 밑에 작성합니다. 예를 들면 이런 식이죠. spec/helpers/extension.rb

기본설정의 RubyMotion프로젝트는 spec helper를 가지고 있지 않습니다.

1.3. Running the Tests

RubyMotion 프로젝트의 테스트 스위트를 실행하기 위해 Rake 태스크의 spec 을 사용합니다.

$ rake spec
$ rake spec:device

이 커맨드는 스팩 프래임워크, 헬퍼 파일이 포함된 특별한 앱을 컴파일하고 백그라운드의 시뮬레이터에서 앱을 실행합니다.

일단 spec이 실행되면, 프로그램은 커맨드라인 프롬프트에 상태 코드(성공일 경우 0, 그 외에는 1)를 돌려줍니다.

1.4. Run Selected Spec Files

전체 스팩코드를 실행하는 대신 개별 spec 파일들을 실행하고 싶을 때가 있습니다.

실행해야 할 spec 파일을 필터링하기 위해, 환경변수 files 에 "," 으로 구분된 일련의 패턴을 설정할 수 있습니다. 패턴은 spec 파일의 basename (확장자가 없는 파일명)이거나, 파일 경로 중 하나여야 합니다.

예를 들어, 밑의 커맨드는 spec/foo_spec.rbspec/bar_spec.rb 파일만 실행할 것입니다.

$ rake spec files=foo_spec,spec/bar_spec.rb

1.5. Output Format

output 환경변수를 지정함으로써 rake spec 의 출력 포맷을 커스터마이징할 수 있습니다. 가능한 출력 포맷은 spec_dox (기본 치), fast, test_unit, tap, knock 가 있습니다.

$ rake spec output=test_unit

2. Basic Testing

assertion의 목록이나 프레임워크에서 지원하는 핵심 predicate는 MacBacon의 README파일을 참고하십시오.

3. Views and Controllers Testing

이 레이어는 애플의UIAutomation를 활용하여 자바스크립트로 테스트를 적도록 강요하지 않으면서 컨트롤러와 뷰의 상호작용의 단위 테스트를 적을 수 있게 합니다.

이것은 명세, runloop 핼퍼, UIView 확장에 사용할 수 있는 작은 API로 구성되어 있습니다.

Important 이것은 전체 애플리케이션의 acceptance 테스트가 가능하다는 의미가 아닙니다. 따라서 정상 모드로 애플리케이션을 실행하셔서는 안 됩니다. 예를 들면, application:didFinishLaunchingWithOptions: 에서 빠르게 종료시키기 위해 RUBYMOTION_ENV 를 사용할 수 있습니다.
class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    return true if RUBYMOTION_ENV == 'test'
    # ...

3.1. Configuring your Context

명세의 context에 어느 컨트롤러를 테스트할지와 필요한 API를 확장할지를 기술할 필요가 있습니다. 기술 방법은 다음과 같습니다.

describe "The 'taking over the world' view" do
  tests TakingOverTheWorldViewController

  # Add your specifications here.
end

이것은 각 명세의 전에 새로운 윈도우, 컨트롤러 클래스의 인스턴스, 뷰 클래스의 인스턴스를 만듭(instantiate)니다.

Tip 만약 윈도우나 컨트롤러에서 독자적인 초기화(custom instantiation)가 필요하다면 before 필터를 tests 보다 먼저 부름으로써 할 수 있습니다. after 필터도 같습니다. 테스트 중에 컨트롤러 인스턴스를 참조하길 원한다면, after 필터를 윈도우와 컨트롤러의 인스턴스를 초기화하는 tests 메소드의 after 필터보다 먼저 불러야 합니다.
describe "Before and after filter order illustrated" do
  # 이 `before` 필터는 `tests` 메소드의 `before` 필터보다 __먼저__ 선언되었으므로, 이 시점에서
  # 윈도우 인스턴스는 아직 생성되지 않았고 컨트롤러의 초기화가 가능하다. 하지만, `window` 부르면
  # 그 자리에서 생성된다는 점을 주의해야 한다.
  before do
    controller.dataThatNeedsToBeConfiguredBeforeAssigningToWindow = ['TableView Row 1', 'TableView Row 2']
  end

  # 이 `after` 필터는 `tests` 메소드의 `after` 필터보다 __먼저__ 선언되었으므로, 이 시점에서
  # 윈도우와 컨트롤러 인스턴스는 지워져서 nil이 되지 않았다.
    controller.performCustomPostTestWork
    window.performCustomPostTestWork
  end

  tests TakingOverTheWorldViewController
  # `tests` 호출하면 밑의 내용 비슷한 `before` 와 `after` 필터가 등록 된다.
  #
  #  before do
  #    createWindowAndControllerIfNotCreatedYet
  #  end
  #
  #  after do
  #    cleanUpWindowAndController
  #  end

  it "performs tests" do
    # ...
  end
end

3.1.1. Storyboards

tests 메소드의 :id 옵션에 Xcode의 컨트롤러 아이디(Identifier)를 넘겨줌으로써, storyboard의 컨트롤러를 테스트할 수 있습니다.

tests StoryboardViewController, :id => 'controller-id'

기본 설정으론, 컨트롤러는 프로젝트의 resources 디렉터리의 MainStoryboard.storyboard 파일에서 로드됩니다. 다른 파일에서 로드하고 싶은 경우, :storyboard 옵션에 스토리보드 파일명을 넘겨주시면 됩니다.

tests StoryboardViewController, :storyboard => 'AlternateStoryboard', :id => 'controller-id'
Tip :id 옵션에 넘겨줄 아이디 필드는 Xcode의 Attributes InspectorView Controller 섹션 에서 찾을 수 있습니다. Attributes Inspector 는 단축키 command-option-4로 열 수 있습니다.

3.2. Durations

어떤 메소드는 :duration 옵션으로 초 단위로 이벤트의 생성 이후의 대기 시간을 지정할 수 있습니다. 이것은 언제나 옵션입니다.

Tip duration의 기본값은 Bacon::Functional.default_duration= 으로 변경 가능합니다.

3.3. Device Events

이 메소드들은 디바이스 레벨의 이벤트를 만듭니다. accessibility레벨의 특정 뷰는 받을 수 없습니다.

3.3.1. rotate_device

디바이스를 지정한 방향으로 회전시킵니다.

rotate_device(:to => orientation, :button => location)
  • to: 회전시킬 방향 :portrait 이나 :landscape 를 넣을 수 있습니다.

  • button: portrait/landscape 의 방향을 설정할 때 사용합니다. portrait에서는 :bottom 이나 :top 을 landscape에서는 :left:right 를 넣을 수 있습니다. 생략했을 경우의 기본 치는 portrait에서는 :bottom, landscape에서는 :left 입니다.

밑의 구문은 홈 버튼이 왼손 쪽으로 오도록 디바이스를 회전시킵니다.

rotate_device :to => :landscape

오른손 쪽으로 회전시킬 수도 있습니다.

rotate_device :to => :landscape, :button => :right

3.3.2. accelerate

accelerometer 이벤트를 생성합니다.

accelerate(:x => x_axis_acceleration, :y => y_axis_acceleration,
           :z => z_axis_acceleration, :duration => duration)
  • x: portrait의 정면을 보고 있는 상태에서, x축은 디바이스의 왼쪽(마이너스)에서 오른쪽(플러스) 방향입니다.

  • y: portrait의 정면을 보고 있는 상태에서, y축은 디바이스의 밑쪽(마이너스)에서 위쪽(플러스) 방향입니다.

  • z: portrait의 정면을 보고 있는 상태에서, z축은 디바이스의 뒤쪽(마이너스)에서 정면 쪽(플러스) 방향입니다.

뒤쪽으로 디바이스를 휘두르는 시뮬레이션은 이렇게 합니다.

accelerate :x => 0, :y => 0, :z => -1

3.3.3. shake

기본적으로 accelerometer 이벤트를 생성합니다.

shake()

흔드는 이벤트를 발생시키고 싶으면 이 메소드를 사용하세요.

더 자세한 정보는 event handling guide를 참조해 주십시오.

3.4. Finding Views

이 메소드들은 뷰를 검색할 때 사용합니다. 이 메소드는 현재 window 에서 뷰 계층 아래쪽으로 내려가며 탐색합니다.

만약 뷰가 검색되지 않으면, time out(기본 치는 3초)될 때까지 재시도 합니다. 이 말은 뷰를 찾을 때 로딩 중이나 애니메이션 중인지 걱정할 필요가 없다는 뜻입니다.

타임아웃 시간이 지나고 뷰를 발견하지 못하면 예외가 발생합니다.

Tip 타임아웃의 기본 치는 Bacon::Functional·default_time out= 으로 변경할 수 있습니다.

3.4.1. view

특정 accessibility 라벨이 있는 뷰를 리턴합니다.

view(label)

Example:

button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
button.setTitle('Take over the world', forState:UIControlStateNormal)
window.addSubview(button)

view('Take over the world') # => button
Tip UIView#viewByName(accessibilityLabel, timeout) 참고.

3.4.2. views

클래스에 해당하는 뷰를 배열로 리턴합니다.

views(view_class)

Example:

button1 = UIButton.buttonWithType(UIButtonTypeRoundedRect)
button1.setTitle('Take over the world', forState:UIControlStateNormal)
window.addSubview(button1)

button2 = UIButton.buttonWithType(UIButtonTypeRoundedRect)
button2.setTitle('But not tonight', forState:UIControlStateNormal)
window.addSubview(button2)

views(UIButton) # => [button1, button2]
Tip UIView#viewsByClass(viewClass, timeout) 참고.

3.5. View Events

이 메소드들은 뷰에서 할 수 있는 모든 오퍼레이션입니다. 오퍼레이션 할 뷰를 지정할 때는 뷰의 ‘accessibility label’ 이나 뷰의 인스턴스에 직접 넘겨서 할 수 있습니다.

Note 일반적으로 모든 UIKit 컨트롤들은 accessibility 라벨의 기본 치를 가지고 있습니다. 예를 들어, “Take over the world”라는 타이틀의 UIButton이 있다면 그 UIButton의 accessibility 라벨은 타이틀과 같은 값이 됩니다. 커스텀 뷰의 경우나 어떤 이유로든 기본 치를 다시 정의해야 할 일이 있다면, accessibilityLabel 속성을 설정해서 할 수 있습니다.

‘location’이 필요할 때는, CGPoint 인스턴스나 밑의 상수(constants) 중 하나를 사용할 수 있습니다.

  • :top_left

  • :top

  • :top_right

  • :right

  • :bottom_right

  • :bottom

  • :bottom_left

  • :left

Note CGPoint 인스턴스는 윈도우 좌표계 안에서만 정의해야 합니다.
Tip 일부 메소드는 옵션으로 :from:to 위치를 받을 수 있습니다. 위의 상수로 :from 이나 :to한쪽만 지정한 경우엔 다른 옵션은 생략할 수 있고 생략된 옵션에는 기본 치가 설정됩니다. 하지만 CG Point 인스턴스를 사용한 경우엔 양쪽 다 설정 해야만 합니다.

3.5.1. tap

뷰 터치 이벤트를 생성합니다.

tap(label_or_view, :at => location, :times => number_of_taps, :touches => number_of_fingers)

밑의 옵션은 전부 생략 가능합니다.

  • at: 터치할 위치를 지정합니다. 기본 치는 뷰의 한 가운데입니다.

  • times: 터치의 횟수를 지정합니다. 기본 치는 한 번입니다.

  • touches: 터치할 손가락 수를 지정합니다. 기본 치는 한 개입니다.

뷰를 한번 터치하려면 이렇게 하면 됩니다.

button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
button.setTitle('Take over the world', forState:UIControlStateNormal)
window.addSubview(button)

tap 'Take over the world'

뷰를 두 손가락으로 두 번 터치하려면 이렇게 하면 됩니다.

view = UIView.alloc.initWithFrame(CGRectMake(0, 0, 100, 100))
view.accessibilityLabel = 'tappable view'
recognizer = UITapGestureRecognizer.alloc.initWithTarget(self, action:'handleTap:')
recognizer.numberOfTapsRequired = 2
recognizer.numberOfTouchesRequired = 2
view.addGestureRecognizer(recognizer)

tap 'tappable view', :times => 2, :touches => 2

3.5.2. flick

짧고 빠른 드래그를 생성합니다.

flick(label_or_view, :from => location, :to => location, :duration => duration)
  • from: 드래그가 시작될 위치.

  • to: 드래그가 끝날 위치.

스위치를 flick하는 이벤트는 이렇게 적습니다.

switch = UISwitch.alloc.initWithFrame(CGRectMake(0, 0, 100, 100))
switch.accessibilityLabel = 'Enable rainbow theme'
window.addSubview(switch)

flick 'Enable rainbow theme', :to => :right

3.5.3. pinch_open

핀치아웃 제스쳐를 생성합니다.

pinch_open(label_or_view, :from => location, :to => location, :duration => duration)
  • from: 손가락의 시작 위치를 지정합니다. 기본 치는 :left 입니다.

  • to: 손가락의 이동이 끝날 위치를 지정합니다. 기본 치는 :right 입니다.

UIScrollView 를 줌인하는 예제입니다.

view('Zooming scrollview').zoomScale # => 1.0
pinch_open 'Zooming scrollview'
view('Zooming scrollview').zoomScale # => 2.0

3.5.4. pinch_close

핀치인 제스쳐를 생성합니다.

pinch_close(label_or_view, :from => location, :to => location, :duration => duration)
  • from: 손가락의 이동이 시작될 위치를 지정합니다. 기본 치는 :right 입니다.

  • to: 손가락의 끝 위치를 지정합니다. 기본 치는 :left 입니다.

UIScrollView 를 줌아웃하는 예제입니다.

view('Zooming scrollview').zoomScale # => 1.0
pinch_close 'Zooming scrollview'
view('Zooming scrollview').zoomScale # => 0.5

3.5.5. drag

지정한 시작, 끝 위치 사이를 드래그하는 이벤트를 생성합니다.

drag(label_or_view, :from => location, :to => location, :number_of_points => steps,
     :points => path, :touches => number_of_fingers, :duration => duration)
  • from: 드래그를 시작할 위치를 지정합니다. :points 옵션이 있는 경우 사용할 수 없습니다.

  • to: 드래그를 끝날 위치를 지정합니다. :points 옵션이 있는 경우 사용할 수 없습니다.

  • number_of_points: :from 에서 :to 까지 드래그 할 때의 지나칠 경로의 수를 지정합니다. 기본 치는 20입니다. :points 옵션이 있는 경우 사용할 수 없습니다.

  • points: 드래그할 패스를 지정합니다. CG Point 인스턴스의 배열을 지정합니다.

  • touches: 드래그에 사용할 손가락 수를 지정합니다. 기본 치는 싱글 터치입니다.

NTOE: 드래그한 방향의 반대 방향으로 스크롤 된다는 점을 주의하십시오.

스크롤 뷰를 밑으로 스크롤하는 예제입니다.

view('Scrollable scrollview').contentOffset.y # => 0
drag 'Scrollable scrollview', :from => :bottom
view('Scrollable scrollview').contentOffset.y # => 400

3.5.6. rotate

뷰의 가운데를 중심으로 시계방향으로 회전시키는 이벤트를 생성합니다.

rotate(label_or_view, :radians => angle, :degrees => angle, :touches => number_of_fingers,
       :duration => duration)
  • radians: 라디안 단위의 회전각을 사용합니다. 기본 치는 π 입니다.

  • degrees: 각도 단위의 회전각을 사용합니다. 기본 치는 180입니다.

  • touches: 회전에 사용할 손가락 수를 지정합니다. 기본 치는 2입니다.