필요한 rpm 설치

npm install typescript jest ts-jest @types/jest ts-node
npm i -D puppeteer jest-puppeteer
npm i -D @types/puppeteer @types/jest-environment-puppeteer @types/expect-puppeteer
npm i -D cross-env

tsconfig.json 수정

{
  "compilerOptions": {
  ..........
    "types": [
      "jest",
      "puppeteer",
      "jest-environment-puppeteer",
      "expect-puppeteer"
    ],
  ..........
  },
  "exclude": [
    "node_modules"
  ]
}

프로젝트 루트에 아래와 깉이 설정 파일들을 생성한다. 

preset.js 생성

const ts_preset = require('ts-jest/jest-preset')
const puppeteer_preset = require('jest-puppeteer/jest-preset')

module.exports = Object.assign(
  ts_preset,
  puppeteer_preset
)

jest.config.js 생성

module.exports = {
    preset: "./preset.js",
    testMatch: ["**/tests/*.test.(ts|tsx)"]
};

jest-puppeteer.config.js

module.exports = {
  launch: {
    headless: false,
    timeout: 20000,
  },
};

package.js에 아래 명령어 추가

//.........
  "scripts": {
    "test": "cross-env DEBUG=test JEST_PUPPETEER_CONFIG=./jest-puppeteer.config.js jest --colors --runInBand --detectOpenHandles --config=./jest.config.js"
  },
//.........

프로젝트 루트에 tests폴더 생성 및 테스트 파일 uitest.test.ts 생성

describe('Naver', () => {
  beforeAll(async () => {
    await page.setViewport({ width: 960, height: 540 });
    await page.goto('https://www.naver.com')
  })

  it('should display "naver" text on page', async () => {
      await expect(page.title()).resolves.toMatch('NAVER');
  })

  it('search keyword', async () => {
    await page.type('input[type=search]', 'develogs', {delay: 20})
    await delay(1000);
    const inputElement = await page.$(
      'button[type=submit]',
    );
    if(inputElement) {
      await inputElement.click();
    }
    await delay(1000);
    await page.screenshot({ path: 'search_result.png' }); // 결과 화면 스크린샷
    await delay(1000);
})
})

function delay(ms: number): Promise<void> {
	return new Promise((resolve, reject) => {
    	setTimeout(() => {
          resolve()
    	}, ms)
    })
}

테스트 명령어

npm run test

UI 테스트 자동화 결과

 

UI 테스트 중 특정 화면을 캡쳐할 수 있음.

 

'Log' 카테고리의 다른 글

carthage에서 googlemaps 사용하기  (1) 2019.10.20
Flutter vs. React Native  (2) 2019.09.28
심심해서 Flutter로 만든 TODO앱  (0) 2019.09.23
cocoapods cache 삭제  (0) 2019.09.22
Flutter sqflite에서 like 사용하기  (0) 2019.09.22

  아직 Swift에서는 DI 라이브러리를 많이 쓰이고 있진 않은 것 같다. github에 DI 라이브러리들이 몇몇 개 보이긴 하는데, Singleton을 지원하지 않거나 class를 key로 쓰고 있어 같은 타입의 object를 등록할 수 없는 등 불편한 점들이 여럿 있었다. 그래서 이런것들을 개선하여 필요한 기능들만 간단하게 만들어 쓰고 있었는데, 여러 프로젝트에 쓰다보니 코드 관리의 필요성이 생겨 라이브러리화 하여 github에 업로드 하였다. 

 

github: https://github.com/ezero9/SwiftInjection

 

Class Summary

Class DIContainer

    func configure()

    func resolve<T>() -> T

    func resolve<T>(key: String) -> T

    func contains(key: String) -> Bool

    func register<T>(_ assemble: @escaping () -> T)

    func register<T>(key: String, _ assemble: @escaping () -> T)

    func registerSingleton<T>(_ assemble: @escaping () -> T)

    func registerSingleton<T>(key: String, _ assemble: @escaping () -> T)

    func registerSingleton<T>(key: String, value: T)

    func destroy()

 

Class DIContainerManager

    func registerContainer<T>(container: T)

    func getObject<T>(key: String) -> T

    func resolve<T>() -> T 

    func destroy()

 

Usage

1.  Register class

  class를 등록하는 방법은 아래 [코드 1]처럼 DIContainer를 상속받아 configure를 override하여 사용하는 방법과 [코드 2]와 같이 DIContainer를 직접 사용하는 방법이 있다.

[코드 1]

 

[코드 2]

2. Get object

  등록된 class는 아래 [코드 3]과 같이 오브젝트를 생성할 수 있다. 

[코드 3]

3. Manage container

  관심사에 따라 여러 컨테이너를 작성하여 물리적으로 분리하고 아래 [코드 4]와 같이 DIContainerManager에 등록하여 사용 할 수 있다. 그리고 상황에 따라 container들을 다르게 생성하여 다른 행동을 하도록 Strategy pattern을 적용할 수 있다.

[코드 4]

 

 

'iOS' 카테고리의 다른 글

Swift에서의 AOP  (0) 2020.08.03
Realm은 thread safe하지 않다.  (0) 2019.09.01
SwiftUI Bata 하루 사용해본 후기  (0) 2019.08.17

  AOP(Aspect-oriented programming)는 횡단 관심과 핵심 관심을 물리적으로 나누고 모듈화하는 것을 말한다. 이는 메소드 호출(Target)을 가로채서 특정 동작(Advice)를 추가해야 하므로 메소드를 Intercept 할 수 있도록 언어에서 기능을 지원해야 한다. 여러 언어에 따라 컴파일 타임 혹은 런타임에 이 기능을 지원하거나 둘 다 지원하기도 하는데, swift는 statically typed language로 메소드 호출을 C++처럼 static/vtable 방식으로 호출한다. 따라서 컴파일 타임에 이를 지원해야 하지만, 안타깝게도 swift 컴파일러는 이를 지원하지 않고 서드파티에서 개발된 관련된 도구도 없음으로 아직 컴파일 타임에 AOP를 사용 할 수 없다.

  Swift 5.1에서 새롭게 공개된 @propertyWrapper를 통해서 변수에 대해 before, after 정도의 AOP를 아래 [코드 1]과 같이 비슷하게 흉내 낼 수 있긴 하지만,  타깃에 어노테이션으로 직접 표시해야 하고 여러 어노테이션을 중첩할 수 없으며(중첩 안되는 건 버그이며 추후 릴리스에 개선 예정이라고 한다.) 메소드에는 아직 적용할 수 없어서 효용성이 크게 있진 않다.

 

[코드 1] Swift5.1의 PropertyWrapper

 

  반면 objective-c는 메시지 기반의 언어이므로 런타임에 메소드 호출을 인터셉트할 수 있다. (실제로 github에 objective-c 용 AOP 라이브러리들이 여러 존재하긴 한다.) 그리고 swift에서는 objective-c API를 호출할 수 있으며, @objc 프로퍼티로 swift class를 objective-c 형태로 초기화하여 사용할 수도 있다. 그리고 UIKit도 objective-c 기반으로 작성되어 있어 UIKit API에도 이 방법으로 AOP를 적용할 수 있다. 

 

  만약 애플리케이션 내에 모든 UIViewController의 viewWillAppear에 google analytics를 적용한다고 한다면, 아래 stackoverflow link에 나와있는 코드처럼 objective-c의 method swizzling을 통해 viewWillAppear에 AOP를 적용해 볼 수 있을 것 같다.

 

https://stackoverflow.com/questions/46417057/want-to-create-a-listener-that-detects-viewwillappear-calls-throughout-the-app

 

  단점은 objective-c이기 때문에 swift보다 메소드 콜 성능이 다소 저하가 발생할 수 있으며, objective-c로 작성된 UIKit과는 달리 Swift로 작성된 SwiftUI에서는 사용할 수 없다. 그리고 기반 코드를 작성하다 보면 배보다 배꼽이 더 커질 수도 있겠다.

 

  Spring에서 AOP를 사용했던 경험이 있어서 그런지 다른 언어로 개발을 하다 보면 AOP에 대한 향수(?)가 굉장히 크다. 객체지향을 좀 더 객체지향답게 코드를 더 깔끔하고 우아하게 만들 수 있는 이 막강한 기능을 사용할 수 없으니 답답한 경우가 굉장히 많다. 하루 빨리 iOS에서도 AOP를 손쉽게 사용할 날이 왔으면 좋겠다.

'iOS' 카테고리의 다른 글

Swift에서의 DI(Dependency Injection)  (0) 2020.08.11
Realm은 thread safe하지 않다.  (0) 2019.09.01
SwiftUI Bata 하루 사용해본 후기  (0) 2019.08.17

+ Recent posts