April 21, 2021
본 포스트는 fp-ts 공식 문서의 Learning Resources에 있는 Getting Started에서 소개하는 문서들을 번역하며 학습한 문서입니다. 원본 문서는 링크에서 확인할 수 있으며 작성한 코드들은 여기에서 확인할 수 있습니다.
때로는 함수형 프로그래밍 스타일로 작성되지 않은 코드와 상호 운영해야 하는 경우가 있습니다. 그러한 상황에서 fp-ts를 이용해 해결하는 방법을 살펴보겠습니다.
Array.prototype.findIndex
Option
import { Option, none, some } from 'fp-ts/Option';
function findIndex<A>(
as: Array<A>,
predicate: (a: A) => boolean
): Option<number> {
const index = as.findIndex(predicate);
return index === -1 ? none : some(index);
}
Array.prototype.findIndex
는 값을 찾지 못했을 경우 -1
이라는 특수한 값을 반환합니다.
Array.prototype.findIndex
함수를 Option
타입을 이용해 fp-ts와 함께 사용할 수 있도록 변경했다. -1
이 반환될 경우 none
을 반환하고 그렇지 않은 경우 some
을 반환하도록 작성했습니다.
작성한 findIndex
함수를 테스트하기 위한 패키지는 아래와 같습니다.
findIndex
함수를 테스트하기 위한 패키지import * as N from 'fp-ts/number';
import { isSome, none, some, getEq, Option, isNone } from 'fp-ts/lib/Option';
import { findIndex } from '../findIndex';
findIndex
함수를 테스트하는 코드테스트코드에서는 findIndex
가 predicate
의 조건에 맞는 값을 찾았을 경우 isSome
함수를 이용해 some
인지 확인한 후 해당 값의 인덱스를 some
으로 감싸 반환된 값과 같은지 확인합니다.
조건에 맞는 값을 찾지 못했을 경우 isNone
함수를 이용해 none
인지 확인한 후 none
과 같은지 확인합니다.
describe('findIndex함수 테스트 (Sentinels)', () => {
let array: number[] = [1, 2, 3, 5];
let result: Option<number>;
const E = getEq(N.Eq);
it('findIndex함수가 존재하는 값을 찾았을 경우', () => {
result = findIndex(array, a => a === 1);
expect(isSome(result)).toBeTruthy();
expect(E.equals(result, some(0))).toBeTruthy();
});
it('findIndex함수가 값을 찾지 못했을 경우', () => {
result = findIndex(array, a => a === 4);
expect(isNone(result)).toBeTruthy();
expect(E.equals(result, none)).toBeTruthy();
});
});
undefined
와 null
undefined
나 null
을 반환할 수 있는 APIArray.prototype.find
Option
, fromNullable
import { Option, fromNullable } from 'fp-ts/Option';
export function find<A>(as: Array<A>, predicate: (a: A) => boolean): Option<A> {
return fromNullable(as.find(predicate));
}
find
함수 테스트하기Array.prototype
에 있는 find
함수는 null
이나 undefined
와 같은 Falsy값을 반환할 수 있습니다.
fromNullable
함수를 이용해 find
함수를 감싸 Option
타입을 반환하는 함수로 만들었습니다.
작성한 find
함수를 테스트하기 위한 패키지는 아래와 같습니다.
find
함수를 테스트하기 위한 패키지import * as N from 'fp-ts/number';
import { isSome, none, some, getEq, Option, isNone } from 'fp-ts/lib/Option';
import { find } from '../find';
find
함수를 테스트하는 코드새로 작성한 find
함수는 값을 찾았을 경우 some
태그를 갖는 값을 반환합니다. 값을 찾았을 경우에는 isSome
메서드가 true
를 반환하는지 확인하고 값이 some
으로 감싸진 값과 같은지 확인합니다.
반면에 값을 찾지 못했을 경우 none
태그를 갖는 값을 반환합니다. 값을 찾지 못했을 경우에는 isNone
메서드가 true
를 반환하는지 확인하고 값이 none
과 같은지 확인합니다.
describe('find함수 테스트 (undefined와 null)', () => {
let array: number[] = [1, 2, 3, 5];
let result: Option<number>;
const E = getEq(N.Eq);
it('find함수가 존재하는 값을 찾았을 경우', () => {
result = find(array, a => a === 1);
expect(isSome(result)).toBeTruthy();
expect(E.equals(result, some(1))).toBeTruthy();
});
it('find함수가 값을 찾지 못했을 경우', () => {
result = find(array, a => a === 4);
expect(isNone(result)).toBeTruthy();
expect(E.equals(result, none)).toBeTruthy();
});
});
throw
될 수 있는 APIJSON.parse
Eiter
, tryCatch
import { Either, tryCatch } from 'fp-ts/Either';
function parse(s: string): Either<Error, unknown> {
return tryCatch(
() => JSON.parse(s),
reason => new Error(String(reason))
);
}
parse
함수 테스트하기JSON.parse
함수는 인자로 잘못된 문자열이 전달될 경우 예외를 발생시킵니다. fp-ts의 tryCatch
를 이용하면 fp-ts와 함께 사용할 수 있도록 Either
타입을 반환시킬 수 있습니다.
작성한 parse
함수를 테스트하기 위한 패키지는 아래와 같습니다.
parse
함수를 테스트하기 위한 패키지import { isLeft, isRight, getOrElse } from 'fp-ts/Either';
import { parse } from '../parse';
parse
함수를 테스트하는 코드parse
함수가 성공하는 값과 실패하는 값을 준비한 후 parse
함수가 정상적으로 실행되는 경우 Either
패키지에 있는 isRight
함수를 사용해 정상적으로 코드가 실행되었는지 확인합니다. 그 후 getOrElse
함수를 이용해 Right
에서 값을 꺼내 기대한 객체와 값이 같은지 확인합니다.
반면에 실패했을 때는 isLeft
함수를 이용해 Left
가 반환되었는지 확인합니다. getOrElse
함수의 인자로 전달된 함수가 반환한 값과 기대하는 값이 같은지 확인합니다.
describe('parse함수 테스트 (예외)', () => {
const success = '{"a": 1, "b": 2}';
const fail = '{"a": 1, "b"}';
let result;
it('parse함수가 정상적으로 실행됐을 경우', () => {
result = parse(success);
expect(isRight(result)).toBeTruthy();
expect(getOrElse(() => ({ error: true }))(result)).toMatchObject({
a: 1,
b: 2,
});
});
it('parse함수 실행 중 예외가 발생했을 경우', () => {
result = parse(fail);
expect(isLeft(result)).toBeTruthy();
expect(getOrElse(() => ({ a: 1 }))(result)).toMatchObject({
a: 1,
});
});
});
Math.random
IO
import { IO } from 'fp-ts/IO';
const random: IO<number> = () => Math.random();
random
함수 테스트하기Math.random
함수는 절대 실패하지 않지만, 비결정론적인 값을 반환합니다. 따라서 fp-ts와 함께 사용하기 위해서 IO
타입으로 감싸 값을 반환하면 됩니다.
작성한 random
함수를 테스트하기 위한 패키지는 아래와 같습니다.
random
함수를 테스트하기 위한 패키지import { random } from '../random';
random
함수를 테스트하는 코드Math.random
은 앞에서 설명했듯이 비결정론적인 값을 반환합니다. 따라서 값을 mocking하도록 도와주는 jest.fn
함수가 반환한 jest.Mock
인스턴스의 mockReturnValue
함수를 이용해 고정적으로 값을 반환할 수 있게 해줍니다. 그 후 mocking된 값이 작성한 random
함수가 반환하는 값과 같은지 확인합니다.
describe('random함수 테스트 (랜덤)', () => {
let result;
it('Math.random함수를 mocking해 테스트하기', () => {
Math.random = jest.fn().mockReturnValue(0.5);
result = random();
expect(result).toBe(0.5);
});
});
localStorage.getItem
IO
import { Option, fromNullable } from 'fp-ts/Option';
import { IO } from 'fp-ts/IO';
function getItem(key: string): IO<Option<string>> {
return () => fromNullable(localStorage.getItem(key));
}
getItem
함수 테스트하기localStorage.getItem
함수는 값을 찾지 못하면 null
을 반환하는 함수입니다. 따라서 fromNullable
을 이용해 값을 찾지 못하면 none
을 반환하고 값을 찾았을 경우 some
을 반환하도록 구현할 수 있습니다.
작성한 getItem
함수를 테스트하기 위한 패키지는 아래와 같습니다.
getItem
함수를 테스트하기 위한 패키지import * as S from 'fp-ts/string';
import { isSome, isNone, none, some, getEq, Option } from 'fp-ts/Option';
import { getItem } from '../getItem';
getItem
함수를 테스트하는 코드localStorage.getItem
함수는 브라우저상에서 사용할 수 있는 함수이므로 jest.fn
을 이용해 원하는 key
가 들어왔을 경우에만 값을 반환하도록 mocking해 줍니다. 여기서 주의해야 할 점은 구현한 getItem
함수는 함수를 반환하는 함수이므로 함수 호출 연산자를 한 번 더 사용해야 합니다.
getItem
함수가 반환한 함수가 성공적으로 값을 가져왔을 경우 some
인지 확인한 후 값이 같은지 비교해주고 값을 찾지 못해 none
을 반환할 경우 none
인지 확인합니다.
describe('getItem함수 테스트 (동기 부수 효과)', () => {
window.localStorage.__proto__.getItem = jest.fn(key => {
if (key === 'success') return 'success';
return null;
});
let result: Option<string>;
const E = getEq(S.Eq);
it('getItem함수가 값을 정상적으로 가져온 경우', () => {
result = getItem('success')();
expect(isSome(result)).toBeTruthy();
expect(E.equals(result, some('success'))).toBeTruthy();
});
it('getItem함수가 값을 가져오지 못했을 경우', () => {
result = getItem('fail')();
expect(isNone(result)).toBeTruthy();
expect(E.equals(result, none)).toBeTruthy();
});
});
throw
할 수있는 APIreadFileSync
IOEither
, tryCatch
import * as fs from 'fs';
import { IOEither, tryCatch } from 'fp-ts/IOEither';
function readFileSync(path: string): IOEither<Error, string> {
return tryCatch(
() => fs.readFileSync(path, 'utf8'),
reason => new Error(String(reason))
);
}
readFileSync
함수 테스트하기fs
의 readFileSync
함수는 파일을 동기적으로 읽어 문자열 값을 반환하는 함수입니다. 매개변수로 전달된 경로에 파일이 없으면 예외를 발생시킵니다.
IOEither
타입과 tryCatch
함수를 이용해 readFileSync
함수를 개선할 수 있습니다. 파일을 정상적으로 읽었을 경우 Right
로 string
을 반환하고 예외가 발생할 경우 Left
로 Error
를 반환합니다.
작성한 readFileSync
함수를 테스트하기 위한 패키지는 아래와 같습니다.
readFileSync
함수를 테스트하기 위한 패키지import * as fs from 'fs';
import { Either, isRight, getOrElse, isLeft } from 'fp-ts/lib/Either';
import { readFileSync } from '../readFileSync';
readFileSync
함수를 테스트하는 코드fs
의 readFileSync
함수는 readonly
이므로 spyOn
을 이용해 값을 mocking해 줍니다. 원하는 경로가 전달되었을 때만 문자열을 반환하고 그렇지 않은 경우 예외를 발생시킵니다. 정상적으로 readFileSync
함수가 실행되었을 경우 Right
타입인지 확인한 후 getOrElse
로 반환된 결과가 mocking된 문자열인지 확인합니다. 예외가 발생했을 경우 Left
타입인지 확인한 후 getOrElse
로 반환된 결과가 getOrElse
함수가 반환한 문자열인지 확인합니다.
describe('readFileSync함수 테스트 (동기 부수 효과)', () => {
jest.spyOn(fs, 'readFileSync').mockImplementation(path => {
if (path === 'success.txt') return 'success';
throw new Error(`${path} is not found.`);
});
let result: Either<Error, string>;
it('readFileSync가 정상적으로 값을 가져왔을 경우', () => {
result = readFileSync('success.txt')();
expect(isRight(result)).toBeTruthy();
expect(getOrElse(() => 'fail')(result)).toBe('success');
});
it('readFileSync함수 실행 중 예외가 발생했을 경우', () => {
result = readFileSync('fail.txt')();
expect(isLeft(result)).toBeTruthy();
expect(getOrElse(() => 'fail')(result)).toBe('fail');
});
});
Task
import { createInterface } from 'readline';
import { Task } from 'fp-ts/Task';
const read: Task<string> = () =>
new Promise<string>(resolve => {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question('', answer => {
rl.close();
resolve(answer);
});
});
read
함수 테스트하기Task
타입은 값을 반환하고 절대로 실패하지 않는 비동기 작업을 나타낼 때 사용합니다. read
함수를 테스트하기 위해서는 readline
의 createInterface
함수를 mocking해야 합니다. createInterface
함수의 qeustion
함수와 close
함수를 아래처럼 mocking 합니다.
read
함수를 테스트하기 위한 createInterface
함수 mocking
__mocks__/readline.js
module.exports = {
createInterface: jest.fn().mockReturnValue({
question: jest.fn().mockImplementationOnce((questionText, cb) => {
cb('success');
}),
close: () => undefined,
}),
};
작성한 read
함수를 테스트하기 위한 패키지는 아래와 같습니다.
read
함수를 테스트하기 위한 패키지import { read } from '../read';
read
함수를 테스트하는 코드jest.mock
함수를 호출해 readline
패키지를 mocking합니다. read
함수는 절대로 실패하지 않으며 비동기 작업을 처리하므로 async
, await
구문을 이용해 read
함수를 호출해 값을 얻어 mocking한 값과 같은지 확인합니다.
jest.mock('readline');
describe('read함수 테스트 (비동기 부수 효과)', () => {
it('read가 정상적으로 값을 읽었을 경우', async () => {
const result = await read();
expect(result).toBe('success');
});
});
fetch
TaskEither
, tryCatch
import { TaskEither, tryCatch } from 'fp-ts/TaskEither';
function get(url: string): TaskEither<Error, string> {
return tryCatch(
() => fetch(url).then(res => res.text()),
reason => new Error(String(reason))
);
}
get
함수 테스트하기fetch
함수는 비동기 작업을 처리하며 예외를 발생시킬 수 있습니다. fetch
함수를 개선하기 위해 TaskEither
타입을 이용해 get
함수를 작성했습니다.
작성한 get
함수를 테스트하기 위한 패키지는 아래와 같습니다.
get
함수를 테스트하기 위한 패키지import { Either, getOrElse, isLeft, isRight } from 'fp-ts/lib/Either';
import { get } from '../get';
get
함수를 테스트하는 코드fetch
함수는 Promise<Response>
를 반환하며 get
함수 내부에서 사용한 text
함수 또한 Promise
를 반환합니다. jest.fn
을 이용해 함수를 mocking하고 특정 문자열이 들어올 경우 Promise.resolve
로 값을 반환해 줍니다. 원하는 문자열이 아닐 경우 Promise.reject
로 거부합니다.
async
, await
구문을 이용해 get
함수로부터 함수를 받아 호출시킵니다. 정상적으로 값을 가져온 경우 isRight
함수로 Right
타입인지 확인한 후 getOrElse
함수로 Right
안의 값을 가져오는지 확인합니다. 거부당한 경우 Left
인지 확인한 후 getOrElse
함수에 전달된 값이 반환되었는지 확인합니다.
describe('get함수 테스트 (비동기 부수 효과)', () => {
global.fetch = jest.fn(url => {
if (url === 'https://success.com') {
return Promise.resolve({
text: () => Promise.resolve('success'),
}) as Promise<Response>;
}
return Promise.reject('fail');
});
let result: Either<Error, string>;
it('get함수가 정상적으로 값을 가져왔을 경우', async () => {
result = await get('https://success.com')();
expect(isRight(result)).toBeTruthy();
expect(getOrElse(() => 'fail')(result)).toBe('success');
});
it('get함수 실행 중 예외가 발생했을 경우', async () => {
result = await get('https://fail.com')();
expect(isLeft(result)).toBeTruthy();
expect(getOrElse(() => 'fail')(result)).toBe('fail');
});
});