1
테스트하기 쉬운 코드로
개발하기
정진욱
2
3
• 정진욱
• EOS 블록체인 미디어 서비스 PUBLYTO 개발
• 최근 관심 분야
• Domain modeling with type system
• Property-based testing
• 페이스북: jinwook.chung.167
• 이메일: mark@publyto.io
• 블로그: https://jwchung.github.io
테스트하기 쉬운 코드란?
4
테스트하기 쉬운 코드란?
• 같은 입력에 항상 같은 결과를 반환하는 코드
public int Add(int x, int y)
{
return x + y;
}
5
테스트하기 쉬운 코드란?
• 같은 입력에 항상 같은 결과를 반환하지 않는 코드 1
public string GetAMOrPM()
{
var now = DateTime.Now;
if (now.Hour < 12)
{
return "AM";
}
else
{
return "PM";
}
}
6
테스트하기 쉬운 코드란?
• 같은 입력에 항상 같은 결과를 반환하지 않는 코드 2
public User FindUserById(int id)
{
return DbContext.Users.FindById(id);
}
7
테스트하기 쉬운 코드란?
• 같은 입력에 항상 같은 결과를 반환하는 코드
• 외부상태를 변경하지 않는 코드
public int Add(int x, int y)
{
return x + y;
}
8
테스트하기 쉬운 코드란?
• 같은 입력에 항상 같은 결과를 반환하는 코드
• 외부상태를 변경하는 코드
public int Add(int x, int y)
{
int result = x + y;
Console.WriteLine(result);
return result;
}
9
테스트하기 쉬운 코드란?
• 같은 입력에 항상 같은 결과를 반환하는 코드
• = 결정적인
• = Deterministic
• 외부 상태를 변경하지 않는 코드
• = 부수효과가 없는 코드
• = No side effects
10
테스트하기 쉬운 코드로 개발하기
본론으로 들어가기
11
들어가기에 앞서
• 예제 코드는 C#으로 작성되어 있습니다.
• 혹시 코드를 놓치신다면 세세한 곳에 얽매이지 마시고, 전체 스토리를 봐
주세요.
12
컨퍼런스 등록 Web API Endpoint
{
"email": "jwchung@hotmail.com",
"name": "Jin-Wook",
"conferenceId": 13,
"seats": 3
}
HTTP POST
HTTP RESPONSE
13
{
"email": "jwchung@hotmail.com",
"name": "Jin-Wook",
"conferenceId": 13,
"seats": 3
} public class ConferenceRegistration
{
public string Email { get; set; }
public string Name { get; set; }
public int ConferenceId { get; set; }
public int Seats { get; set; }
}
매핑
컨퍼런스 등록 Web API Endpoint
14
컨퍼런스 등록 Web API Endpoint
public class ConferenceRegistrationsController : ControllerBase
{
public ActionResult Post(ConferenceRegistration registration)
{
throw new NotImplementedException();
}
}
15
컨퍼런스 등록 단계
1. ConferenceRegistration 유효성 검사
public class ConferenceRegistration
{
public string Email { get; set; }
public string Name { get; set; }
public int ConferenceId { get; set; }
public int Seats { get; set; }
}
16
컨퍼런스 등록 단계
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
17
컨퍼런스 등록 단계
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
3. 요청한 좌석 수가 확보 가능한지 판단
• 전체 좌석 수 – 등록된 좌석 수 >= 요청한 좌석수
• 한꺼번에10좌석을 초과해서 등록할 수 없다
18
컨퍼런스 등록 단계
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
3. 요청한 좌석 수가 확보 가능한지 판단
4. 등록 정보 저장
5. HTTP 결과 반환
19
테스트하기 어려운 단계는?
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
3. 요청한 좌석 수가 확보 가능한지 판단
4. 등록 정보 저장
5. HTTP 결과 반환
20
테스트하기 어려운 단계는?
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
3. 요청한 좌석 수가 확보 가능한지 판단
4. 등록 정보 저장
5. HTTP 결과 반환
21
테스트하기 어려운 단계는?
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
3. 요청한 좌석 수가 확보 가능한지 판단
4. 등록 정보 저장
5. HTTP 결과 반환
22
2, 3 단계를 함께 구현해보자
2. 이미 등록된 좌석 수 DB에서 읽어오기
3. 요청한 좌석 수가 확보 가능한지 판단
• 전체 좌석 수 – 등록된 좌석 수 >= 요청한 좌석수
• 한꺼번에 10좌석을 초과해서 등록할 수 없다
23
public bool CanBeRegistered(int capacity, int conferenceId, int requestSeats)
{
using (var dbContext = new ConferenceDbContext())
{
IQueryable<int> query =
from registration in dbContext.ConferenceRegistrations
where registration.ConferenceId == conferenceId
select registration.Seats;
int registeredSeats = query.Sum();
}
}
select sum(Seats)
from ConferenceRegistrations
where ConferenceId = 13
24
public bool CanBeRegistered(int capacity, int conferenceId, int requestSeats)
{
if (requestSeats > 10)
{
return false;
}
using (var dbContext = new ConferenceDbContext())
{
IQueryable<int> query =
from registration in dbContext.ConferenceRegistrations
where registration.ConferenceId == conferenceId
select registration.Seats;
int registeredSeats = query.Sum();
if (capacity - registeredSeats >= requestSeats)
{
return true;
}
return false;
}
}
25
public bool CanBeRegistered(int capacity, int conferenceId, int requestSeats)
{
if (requestSeats > 10)
{
return false;
}
using (var dbContext = new ConferenceDbContext())
{
IQueryable<int> query =
from registration in dbContext.ConferenceRegistrations
where registration.ConferenceId == conferenceId
select registration.Seats;
int registeredSeats = query.Sum();
if (capacity - registeredSeats >= requestSeats)
{
return true;
}
return false;
}
}
테스트하기 쉬운 코드와
어려운 코드를 구분한다면?
26
public bool CanBeRegistered(int capacity, int conferenceId, int requestSeats)
{
if (requestSeats > 10)
{
return false;
}
using (var dbContext = new ConferenceDbContext())
{
IQueryable<int> query =
from registration in dbContext.ConferenceRegistrations
where registration.ConferenceId == conferenceId
select registration.Seats;
int registeredSeats = query.Sum();
if (capacity - registeredSeats >= requestSeats)
{
return true;
}
return false;
}
}
27
무엇이 문제인가?
3. 요청한 좌석 수가 확보 가능한지 판단
• 전체 좌석 수 – 등록된 좌석 수 >= 요청한 좌석수
• 한꺼번에 10좌석을 초과해서 등록할 수 없다
을 테스트하기 위해 DB에 테스트 데이터를 설정해야 한다.
28
public bool CanBeRegistered(int capacity, int conferenceId, int requestSeats)
{
if (requestSeats > 10)
{
return false;
}
using (var dbContext = new ConferenceDbContext())
{
IQueryable<int> query =
from registration in dbContext.ConferenceRegistrations
where registration.ConferenceId == conferenceId
select registration.Seats;
int registeredSeats = query.Sum();
if (capacity - registeredSeats >= requestSeats)
{
return true;
}
return false;
}
}
29
어떻게 해야 할까요?
30
분리합시다
31
분리합시다
public int QueryRegisteredSeats(int conferenceId)
{
using (var dbContext = new ConferenceDbContext())
{
IQueryable<int> query =
from registration in dbContext.ConferenceRegistrations
where registration.ConferenceId == conferenceId
select registration.Seats;
return query.Sum();
}
}
32
분리합시다
public bool CanBeRegistered(
int capacity,
int registeredSeats,
int requestSeats)
{
if (requestSeats > 10)
{
return false;
}
if (capacity - registeredSeats >= requestSeats)
{
return true;
}
return false;
} 33
public class ConferenceRepository
{
public int QueryRegisteredSeats(int conferenceId)
{
using (var dbContext = new ConferenceDbContext())
{
IQueryable<int> query =
from registration in dbContext.ConferenceRegistrations
where registration.ConferenceId == conferenceId
select registration.Seats;
return query.Sum();
}
}
}
public class ConferenceRegistration
{
public string Email { get; set; }
public string Name { get; set; }
public int ConferenceId { get; set; }
public int Seats { get; set; }
public bool CanBeRegistered(int capacity, int registeredSeats, int requestSeats)
{
if (requestSeats > 10)
{
return false;
}
if (capacity - registeredSeats >= requestSeats)
{
return true;
}
return false;
}
}
public class ConferenceRegistration
{
public string Email { get; set; }
public string Name { get; set; }
public int ConferenceId { get; set; }
public int Seats { get; set; }
public bool CanBeRegistered(int capacity, int registeredSeats)
{
if (requestSeats > 10)
{
return false;
}
if (capacity - registeredSeats >= requestSeats)
{
return true;
}
return false;
}
}
public class ConferenceRegistration
{
public string Email { get; set; }
public string Name { get; set; }
public int ConferenceId { get; set; }
public int Seats { get; set; }
public bool CanBeRegistered(int capacity, int registeredSeats)
{
if (this.Seats > 10)
{
return false;
}
if (capacity - registeredSeats >= requestSeats)
{
return true;
}
return false;
}
}
public class ConferenceRegistration
{
public string Email { get; set; }
public string Name { get; set; }
public int ConferenceId { get; set; }
public int Seats { get; set; }
public bool CanBeRegistered(int capacity, int registeredSeats)
{
if (this.Seats > 10)
{
return false;
}
if (capacity - registeredSeats >= this.Seats)
{
return true;
}
return false;
}
}
정리
• 테스트하기 쉬운 코드란?
• 항상 같은 결과 반환
• 외부상태를 변경하지 않음
• 테스트하기 쉬운 코드로 개발하기
• 방법1: 테스트하기 쉬운코드와 어려운 코드 분리
• 방법2: 두 부류의 코드는 어디서 만나야 하나?
• 방법3:
39
TDD 맛보기
40
컨퍼런스 등록하기 단계
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
3. 요청한 좌석 수가 확보 가능한지 판단
4. 등록 정보 저장
5. HTTP 결과 반환
41
컨퍼런스 등록하기 단계
1. ConferenceRegistration 유효성 검사
42
[Fact]
public void 이메일에_골뱅이가_없으면_유효하지_않은_형식입니다()
{
// Arrange
var sut = new ConferenceRegistration { Email = "jwchung_hotmail.com" };
// Act
string actual = sut.Validate();
// Assert
Assert.NotNull(actual);
}
43
public class ConferenceRegistration
{
public string Email { get; set; }
public string Name { get; set; }
public int ConferenceId { get; set; }
public int Seats { get; set; }
public string Validate()
{
return "이메일은 @를 포함해야 합니다.";
}
}
?
44
테스트 코드
45
[Fact]
public void 이메일에_골뱅이가_있으면_유효한_형식입니다()
{
// Arrange
var sut = new ConferenceRegistration { Email = "jwchung@hotmail.com" };
// Act
string actual = sut.Validate();
// Assert
Assert.Null(actual);
}
46
public class ConferenceRegistration
{
public string Email { get; set; }
public string Name { get; set; }
public int ConferenceId { get; set; }
public int Seats { get; set; }
public string Validate()
{
if (this.Email.Contains("@"))
return null;
return "이메일은 @를 포함해야 합니다.";
}
}
47
더 많은 테스트케이스는 생략하겠습니다.
• 이메일 도메인 파트에 닷(.)이 들어가야합니다.
• 이름은 숫자가 포함되면 안됩니다.
• 요청 좌석수는 양수여야 합니다.
• 컨퍼런스 아이디도 양수입니다.
• …
48
컨퍼런스 등록하기 단계
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
49
[Fact]
public void 컨퍼런스Id로_기_등록된_좌석_수를_조회할_수_있습니다()
{
// Arrange
using (var context = new ConferenceDbContext())
{
context.ConferenceRegistrations.Add(
new ConferenceRegistration
{
Email = "foo@bar.com", Name = "foo",
ConferenceId = 2, Seats = 2
});
context.ConferenceRegistrations.Add(
new ConferenceRegistration
{
Email = "bar@baz.com", Name = "bar"
ConferenceId = 13, Seats = 3
});
context.SaveChanges();
}
var sut = new ConferenceRepository();
... 50
//using {...
context.ConferenceRegistrations.Add(
new ConferenceRegistration
{
Email = "bar@baz.com", Name = "bar"
ConferenceId = 13, Seats = 3
});
context.SaveChanges();
}
var sut = new ConferenceRepository();
// Act
int actual = sut.QueryRegisteredSeats(13);
// Assert
Assert.Equal(3, actual);
}
51
public class ConferenceRepository
{
public int QueryRegisteredSeats(int conferenceId)
{
return 3;
}
}
52
더 많은 조합을 테스트하면…
// Arrange
using (var context = new ConferenceDbContext())
{
context.ConferenceRegistrations.Add(
new ConferenceRegistration
{
Email = "foo@bar.com", Name = "foo",
ConferenceId = 2, Seats = 2
});
context.ConferenceRegistrations.Add(
new ConferenceRegistration
{
Email = "bar@baz.com", Name = "bar"
ConferenceId = 13, Seats = 3
});
context.SaveChanges();
}
53
public class ConferenceRepository
{
public int QueryRegisteredSeats(int conferenceId)
{
using (var dbContext = new ConferenceDbContext())
{
IQueryable<int> query =
from registration in dbContext.ConferenceRegistrations
where registration.ConferenceId == conferenceId
select registration.Seats;
return query.Sum();
}
}
}
54
컨퍼런스 등록하기 단계
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
55
컨퍼런스 등록하기 단계
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
56
컨퍼런스 등록하기 단계
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
3. 요청한 좌석 수가 확보 가능한지 판단
4. 등록 정보 저장
5. HTTP 결과 반환
57
컨퍼런스 등록하기 단계
1. ConferenceRegistration 유효성 검사
2. 이미 등록된 좌석 수 DB에서 읽어오기
3. 요청한 좌석 수가 확보 가능한지 판단
4. 등록 정보 저장
5. HTTP 결과 반환
58
59
public class ConferenceRepository : IConferenceRepository
{
public int QueryRegisteredSeats(int conferenceId) { ... }
public void Save(ConferenceRegistration registration) { ... }
}
public class ConferenceRegistration
{
public string Email { get; set; }
public string Name { get; set; }
public int ConferenceId { get; set; }
public int Seats { get; set; }
public string Validate() { ... }
public bool CanBeRegistered(int capacity, int registeredSeats) { ... }
}
정리
• 테스트하기 쉬운 코드란?
• 항상 같은 결과 반환
• 외부상태를 변경하지 않음
• 테스트하기 쉬운 코드로 개발하기
• 방법1: 테스트하기 쉬운코드와 어려운 코드 분리
• 방법2: 두 부류의 코드는 어디서 만나야 하나?
• 방법3:
60
두 부류의 코드는 어디서 만나야 하나?
61
두 부류의 코드는 어디서 만나야 하나?
62
두 부류의 코드는 어디서 만나야 하나?
63
public bool CanBeRegistered(int capacity, int conferenceId, int requestSeats)
{
if (requestSeats > 10)
{
return false;
}
using (var dbContext = new ConferenceDbContext())
{
IQueryable<int> query =
from registration in dbContext.ConferenceRegistrations
where registration.ConferenceId == conferenceId
select registration.Seats;
int registeredSeats = query.Sum();
if (capacity - registeredSeats >= requestSeats)
{
return true;
}
return false;
}
}
64
두 부류의 코드는 어디서 만나야 하나?
65
두 부류의 코드는 어디서 만나야 하나?
66
어떻게 해야 할까요?
67
어떻게 해야 할까요?
68
분리해서
69
최대한 가장자리에서 만나게 합니다.
70
최대한 가장자리에서 만나게 합니다.
71
72
public class ConferenceRegistrationsController : ControllerBase
{
public ActionResult Post(ConferenceRegistration registration)
{
string error = registration.Validate();
if (error != null)
return this.BadRequest(error);
int conferenceId = registration.ConferenceId;
var repository = new ConferenceRepository();
int registeredSeats = repository.QueryRegisteredSeats(conferenceId);
int capacity = 300;
bool canBeRegistered = registration.CanBeRegistered(capacity, registeredSeats);
if (!canBeRegistered)
return this.BadRequest("잔여석이 없거나, 10좌석을 초과 신청한 경우입니다.");
repository.Save(registration);
return this.Ok("신청되셨습니다.");
}
}
정리3
• 테스트하기 쉬운 코드란?
• TDD 맛보기
• 테스트하기 쉬운 코드로 개발하기
• 방법1: 테스트하기 쉬운코드와 어려운 코드 분리
• 방법2: 두 부류의 코드는 최대한 가장 자리에 위치
• 방법3: 두 부류 코드가 만나는 가장자리는 어떻게 테스트하나?
73
(예외: 로깅, 퍼사드 )
가장자리는 어떻게 테스트하는가?
• 수동테스트
• 자동테스트
74
수동테스트
75
$ curl -X POST http://okkycon.com/api/conference-registrations/ ...
select top 10 *
from ConferenceRegistrations
where ConferenceId = 13
자동테스트
76
77
[Fact]
public void 잔여석이_남은_컨퍼런스는_등록이_가능합니다()
{
var sut = new ConferenceRegistrationsController();
var registration = new ConferenceRegistration
{
Email = "jwchung@hotmail.com", Name = "Jin-Wook Chung",
ConferenceId = 13, Seats = 2
};
ActionResult actual = sut.Post(registration);
Assert.IsType<OkObjectResult>(actual);
using (var context = new ConferenceDbContext())
{
var query = from r in context.ConferenceRegistrations
where r.Email == registration.Email
&& r.ConferenceId == registration.ConferenceId
select r.Seats;
var actualSeats = query.First();
Assert.Equal(2, actualSeats);
}
}
78
public ActionResult Post(ConferenceRegistration registration)
{
string error = registration.Validate();
if (error != null)
return this.BadRequest(error);
int conferenceId = registration.ConferenceId;
var repository = new ConferenceRepository();
int registeredSeats = repository.QueryRegisteredSeats(conferenceId);
int capacity = 300;
bool canBeRegistered = registration.CanBeRegistered(capacity,
registeredSeats);
if (!canBeRegistered)
return this.BadRequest("잔여석이 없거나, 10좌석을 초과 신청한 경우입니다.");
repository.Save(registration);
return this.Ok("신청되셨습니다.");
}
79
public ActionResult Post(ConferenceRegistration registration)
{
var repository = new ConferenceRepository();
repository.Save(registration);
return this.Ok("신청되셨습니다.");
}
문제는…
80
public ActionResult Post(ConferenceRegistration registration)
{
using (var context = new ConferenceDbContext())
{
context.Database.ExecuteSqlCommand(
"insert into ConferenceRegistrations values({0},{1},{2},{3})",
registration.Email,
registration.Name,
registration.ConferenceId,
registration.Seats);
}
return this.Ok("신청되셨습니다.");
}
작성된 코드 사용을 강제할 수 있나?
• 현 상태의 코드(디자인)로는 할 수 없다.
• 실제 클래스 대신 목(Mock) 사용을 위해 이음새(Seam)가 있어야 한다.
81
이음새(Seam) 도입
82
public interface IConferenceRepository
{
int QueryRegisteredSeats(int conferenceId);
void Save(ConferenceRegistration registration);
}
이음새(Seam) 도입
83
public class ConferenceRegistrationsController : ControllerBase
{
public ConferenceRegistrationsController(IConferenceRepository repository)
{
this.Repository = repository;
}
public IConferenceRepository Repository { get; }
public ActionResult Post(ConferenceRegistration registration) { ... }
}
이음새(Seam) 도입
84
public class ConferenceRepository : IConferenceRepository
{
public int QueryRegisteredSeats(int conferenceId) { ... }
public void Save(ConferenceRegistration registration) { ... }
}
수동 목(Mock) 작성
85
public class MockRepository : IConferenceRepository
{
public int ConferenceId { get; set; }
public int RegisteredSeats { get; set; }
public ConferenceRegistration Registration { get; set; }
public int QueryRegisteredSeats(int conferenceId)
{
this.ConferenceId = conferenceId;
return RegisteredSeats;
}
public void Save(ConferenceRegistration registration)
{
this.Registration = registration;
}
}
86
[Fact]
public void 목을_사용해도_잔여석이_남은_컨퍼런스는_등록이_가능합니다()
{
var mock = new MockRepository { RegisteredSeats = 298 };
var sut = new ConferenceRegistrationsController(mock);
var registration = new ConferenceRegistration
{
Email = "jwchung@hotmail.com",
Name = "Jin-Wook Chung",
ConferenceId = 13,
Seats = 2
};
ActionResult actual = sut.Post(registration);
Assert.IsType<OkObjectResult>(actual);
Assert.Equal(mock.Registration, registration);
Assert.Equal(mock.ConferenceId, registration.ConferenceId);
}
87
public class MockRepository : IConferenceRepository
{
public int ConferenceId { get; set; }
public int RegisteredSeats { get; set; }
public ConferenceRegistration Registration { get; set; }
public int QueryRegisteredSeats(int conferenceId)
{
this.ConferenceId = conferenceId;
return RegisteredSeats;
}
public void Save(ConferenceRegistration registration)
{
this.Registration = registration;
}
}
88
[Fact]
public void 목을_사용해도_잔여석이_남은_컨퍼런스는_등록이_가능합니다()
{
var mock = new MockRepository { RegisteredSeats = 298 };
var sut = new ConferenceRegistrationsController(mock);
var registration = new ConferenceRegistration
{
Email = "jwchung@hotmail.com",
Name = "Jin-Wook Chung",
ConferenceId = 13,
Seats = 2
};
ActionResult actual = sut.Post(registration);
Assert.IsType<OkObjectResult>(actual);
Assert.Equal(mock.Registration, registration);
Assert.Equal(mock.ConferenceId, registration.ConferenceId);
}
89
public class MockRepository : IConferenceRepository
{
public int ConferenceId { get; set; }
public int RegisteredSeats { get; set; }
public ConferenceRegistration Registration { get; set; }
public int QueryRegisteredSeats(int conferenceId)
{
this.ConferenceId = conferenceId;
return RegisteredSeats;
}
public void Save(ConferenceRegistration registration)
{
this.Registration = registration;
}
}
90
[Fact]
public void 목을_사용해도_잔여석이_남은_컨퍼런스는_등록이_가능합니다()
{
var mock = new MockRepository { RegisteredSeats = 298 };
var sut = new ConferenceRegistrationsController(mock);
var registration = new ConferenceRegistration
{
Email = "jwchung@hotmail.com",
Name = "Jin-Wook Chung",
ConferenceId = 13,
Seats = 2
};
ActionResult actual = sut.Post(registration);
Assert.IsType<OkObjectResult>(actual);
Assert.Equal(mock.Registration, registration);
Assert.Equal(mock.ConferenceId, registration.ConferenceId);
}
91
public class MockRepository : IConferenceRepository
{
public int ConferenceId { get; set; }
public int RegisteredSeats { get; set; }
public ConferenceRegistration Registration { get; set; }
public int QueryRegisteredSeats(int conferenceId)
{
this.ConferenceId = conferenceId;
return RegisteredSeats;
}
public void Save(ConferenceRegistration registration)
{
this.Registration = registration;
}
}
92
public IConferenceRepository Repository { get; }
public ActionResult Post(ConferenceRegistration registration)
{
string error = registration.Validate();
if (error != null)
return this.BadRequest(error);
int conferenceId = registration.ConferenceId;
var repository = new ConferenceRepository();
int registeredSeats = repository
.QueryRegisteredSeats(conferenceId);
int capacity = 300;
bool canBeRegistered = registration.CanBeRegistered(capacity, registeredSeats);
if (!canBeRegistered)
return this.BadRequest("잔여석이 없거나, 10좌석을 초과 신청한 경우입니다.");
repository.Save(registration);
return this.Ok("신청되셨습니다.");
}
93
public IConferenceRepository Repository { get; }
public ActionResult Post(ConferenceRegistration registration)
{
string error = registration.Validate();
if (error != null)
return this.BadRequest(error);
int conferenceId = registration.ConferenceId;
int registeredSeats = this.Repository
.QueryRegisteredSeats(conferenceId);
int capacity = 300;
bool canBeRegistered = registration.CanBeRegistered(capacity, registeredSeats);
if (!canBeRegistered)
return this.BadRequest("잔여석이 없거나, 10좌석을 초과 신청한 경우입니다.");
this.Repository.Save(registration);
return this.Ok("신청되셨습니다.");
}
목(Mock) 사용
• 작성된 코드 사용을 강제할 수 있다.
• 목 사용이 큰 장점으로 보이지만, 생각해볼 점이 많다.
94
95
행위 검증
행위가
호출되었는가
Mockist
불필요한 추상화
유발 가능성
상태 검증
결과 값이
무엇인가
Classicist
불필요한 추상화
필요 없음
96
행위 검증
구현되지 않는
코드에 의존 가능
Outside-in
접근법
클라이언트
시각과 같은 방향
상태 검증
구현된 코드에만
의존
Inside-out
접근법
클라이언트
시각과 반대방향
97
상태검증(Inside-out)
행위검증(Ouside-in)
제가 생각하는 목(Mock) 사용 문제점
• 목을 남발할 가능성이 크다.
• 대부분 목 사용 예제는 간단하다. 그래서 장점이 크게 보인다.
• 실제 프로젝트에 적용하면 한꺼번에 많은 수의 목을 다루면서 곤란을 겪는다.
• 적당 수의 목 사용에 대한 답을 찾기 어렵다.
• 상태 검증으로 돌아가보자.
98
99
[Fact]
public void 잔여석이_남은_컨퍼런스는_등록이_가능합니다()
{
var sut = new ConferenceRegistrationsController();
var registration = new ConferenceRegistration
{
Email = "jwchung@hotmail.com", Name = "Jin-Wook Chung",
ConferenceId = 13, Seats = 2
};
ActionResult actual = sut.Post(registration);
Assert.IsType<OkObjectResult>(actual);
using (var context = new ConferenceDbContext())
{
var query = from r in context.ConferenceRegistrations
where r.Email == registration.Email
&& r.ConferenceId == registration.ConferenceId
select r.Seats;
var actualSeats = query.First();
Assert.Equal(2, actualSeats);
}
}
100
public ActionResult Post(ConferenceRegistration registration)
{
using (var context = new ConferenceDbContext())
{
context.Database.ExecuteSqlCommand(
"insert into ConferenceRegistrations values({0},{1},{2},{3})",
registration.Email,
registration.Name,
registration.ConferenceId,
registration.Seats);
}
return this.Ok("신청되셨습니다.");
}
상태 검증 - 문제 극복 방안
• TDD를 통한 사전이 아니라 사후 테스트를 하자.
• 두 부류의 코드가 맞물려 잘 돌아가는 로직이다.
• 난해한 코드가 아니다.
• 구현된 코드를 사용하지 않고 굳이 어려운 길을 택할 이유가 없다.
• 완벽을 추구하면서 목을 사용하는 비용을 들일 필요가 있는가
101
정리4(최종)
• 테스트하기 쉬운 코드란?
• 테스트하기 쉬운 코드로 개발하기
• 방법1: 테스트하기 쉬운코드와 어려운 코드 분리
• 방법2: 두 부류의 코드는 최대한 가장 자리에 위치
• 방법3: 가장 자리를 테스트하는 방법을 익히자
• 수동
• 자동: 상태검증 / 행위검증
102
끝으로
• 요즘 저는
• 두 부류 코드를 분리해서 각각 테스트하고,
• 가장자리에서 맞물려 돌아가는 코드는 주로 수동테스트합니다.
• 두 부류 코드 섞어 넣고 테스트가 어렵다고 포기하지 마세요.
• 위 내용과 관련된 제 블로그 글이 있답니다.
• http://jwchung.github.io/testing-oh-my
103
경청해주셔서 감사합니다.
104

[OKKYCON] 정진욱 - 테스트하기 쉬운 코드로 개발하기

  • 1.
  • 2.
  • 3.
    3 • 정진욱 • EOS블록체인 미디어 서비스 PUBLYTO 개발 • 최근 관심 분야 • Domain modeling with type system • Property-based testing • 페이스북: jinwook.chung.167 • 이메일: mark@publyto.io • 블로그: https://jwchung.github.io
  • 4.
  • 5.
    테스트하기 쉬운 코드란? •같은 입력에 항상 같은 결과를 반환하는 코드 public int Add(int x, int y) { return x + y; } 5
  • 6.
    테스트하기 쉬운 코드란? •같은 입력에 항상 같은 결과를 반환하지 않는 코드 1 public string GetAMOrPM() { var now = DateTime.Now; if (now.Hour < 12) { return "AM"; } else { return "PM"; } } 6
  • 7.
    테스트하기 쉬운 코드란? •같은 입력에 항상 같은 결과를 반환하지 않는 코드 2 public User FindUserById(int id) { return DbContext.Users.FindById(id); } 7
  • 8.
    테스트하기 쉬운 코드란? •같은 입력에 항상 같은 결과를 반환하는 코드 • 외부상태를 변경하지 않는 코드 public int Add(int x, int y) { return x + y; } 8
  • 9.
    테스트하기 쉬운 코드란? •같은 입력에 항상 같은 결과를 반환하는 코드 • 외부상태를 변경하는 코드 public int Add(int x, int y) { int result = x + y; Console.WriteLine(result); return result; } 9
  • 10.
    테스트하기 쉬운 코드란? •같은 입력에 항상 같은 결과를 반환하는 코드 • = 결정적인 • = Deterministic • 외부 상태를 변경하지 않는 코드 • = 부수효과가 없는 코드 • = No side effects 10
  • 11.
    테스트하기 쉬운 코드로개발하기 본론으로 들어가기 11
  • 12.
    들어가기에 앞서 • 예제코드는 C#으로 작성되어 있습니다. • 혹시 코드를 놓치신다면 세세한 곳에 얽매이지 마시고, 전체 스토리를 봐 주세요. 12
  • 13.
    컨퍼런스 등록 WebAPI Endpoint { "email": "jwchung@hotmail.com", "name": "Jin-Wook", "conferenceId": 13, "seats": 3 } HTTP POST HTTP RESPONSE 13
  • 14.
    { "email": "jwchung@hotmail.com", "name": "Jin-Wook", "conferenceId":13, "seats": 3 } public class ConferenceRegistration { public string Email { get; set; } public string Name { get; set; } public int ConferenceId { get; set; } public int Seats { get; set; } } 매핑 컨퍼런스 등록 Web API Endpoint 14
  • 15.
    컨퍼런스 등록 WebAPI Endpoint public class ConferenceRegistrationsController : ControllerBase { public ActionResult Post(ConferenceRegistration registration) { throw new NotImplementedException(); } } 15
  • 16.
    컨퍼런스 등록 단계 1.ConferenceRegistration 유효성 검사 public class ConferenceRegistration { public string Email { get; set; } public string Name { get; set; } public int ConferenceId { get; set; } public int Seats { get; set; } } 16
  • 17.
    컨퍼런스 등록 단계 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 17
  • 18.
    컨퍼런스 등록 단계 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 3. 요청한 좌석 수가 확보 가능한지 판단 • 전체 좌석 수 – 등록된 좌석 수 >= 요청한 좌석수 • 한꺼번에10좌석을 초과해서 등록할 수 없다 18
  • 19.
    컨퍼런스 등록 단계 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 3. 요청한 좌석 수가 확보 가능한지 판단 4. 등록 정보 저장 5. HTTP 결과 반환 19
  • 20.
    테스트하기 어려운 단계는? 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 3. 요청한 좌석 수가 확보 가능한지 판단 4. 등록 정보 저장 5. HTTP 결과 반환 20
  • 21.
    테스트하기 어려운 단계는? 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 3. 요청한 좌석 수가 확보 가능한지 판단 4. 등록 정보 저장 5. HTTP 결과 반환 21
  • 22.
    테스트하기 어려운 단계는? 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 3. 요청한 좌석 수가 확보 가능한지 판단 4. 등록 정보 저장 5. HTTP 결과 반환 22
  • 23.
    2, 3 단계를함께 구현해보자 2. 이미 등록된 좌석 수 DB에서 읽어오기 3. 요청한 좌석 수가 확보 가능한지 판단 • 전체 좌석 수 – 등록된 좌석 수 >= 요청한 좌석수 • 한꺼번에 10좌석을 초과해서 등록할 수 없다 23
  • 24.
    public bool CanBeRegistered(intcapacity, int conferenceId, int requestSeats) { using (var dbContext = new ConferenceDbContext()) { IQueryable<int> query = from registration in dbContext.ConferenceRegistrations where registration.ConferenceId == conferenceId select registration.Seats; int registeredSeats = query.Sum(); } } select sum(Seats) from ConferenceRegistrations where ConferenceId = 13 24
  • 25.
    public bool CanBeRegistered(intcapacity, int conferenceId, int requestSeats) { if (requestSeats > 10) { return false; } using (var dbContext = new ConferenceDbContext()) { IQueryable<int> query = from registration in dbContext.ConferenceRegistrations where registration.ConferenceId == conferenceId select registration.Seats; int registeredSeats = query.Sum(); if (capacity - registeredSeats >= requestSeats) { return true; } return false; } } 25
  • 26.
    public bool CanBeRegistered(intcapacity, int conferenceId, int requestSeats) { if (requestSeats > 10) { return false; } using (var dbContext = new ConferenceDbContext()) { IQueryable<int> query = from registration in dbContext.ConferenceRegistrations where registration.ConferenceId == conferenceId select registration.Seats; int registeredSeats = query.Sum(); if (capacity - registeredSeats >= requestSeats) { return true; } return false; } } 테스트하기 쉬운 코드와 어려운 코드를 구분한다면? 26
  • 27.
    public bool CanBeRegistered(intcapacity, int conferenceId, int requestSeats) { if (requestSeats > 10) { return false; } using (var dbContext = new ConferenceDbContext()) { IQueryable<int> query = from registration in dbContext.ConferenceRegistrations where registration.ConferenceId == conferenceId select registration.Seats; int registeredSeats = query.Sum(); if (capacity - registeredSeats >= requestSeats) { return true; } return false; } } 27
  • 28.
    무엇이 문제인가? 3. 요청한좌석 수가 확보 가능한지 판단 • 전체 좌석 수 – 등록된 좌석 수 >= 요청한 좌석수 • 한꺼번에 10좌석을 초과해서 등록할 수 없다 을 테스트하기 위해 DB에 테스트 데이터를 설정해야 한다. 28
  • 29.
    public bool CanBeRegistered(intcapacity, int conferenceId, int requestSeats) { if (requestSeats > 10) { return false; } using (var dbContext = new ConferenceDbContext()) { IQueryable<int> query = from registration in dbContext.ConferenceRegistrations where registration.ConferenceId == conferenceId select registration.Seats; int registeredSeats = query.Sum(); if (capacity - registeredSeats >= requestSeats) { return true; } return false; } } 29
  • 30.
  • 31.
  • 32.
    분리합시다 public int QueryRegisteredSeats(intconferenceId) { using (var dbContext = new ConferenceDbContext()) { IQueryable<int> query = from registration in dbContext.ConferenceRegistrations where registration.ConferenceId == conferenceId select registration.Seats; return query.Sum(); } } 32
  • 33.
    분리합시다 public bool CanBeRegistered( intcapacity, int registeredSeats, int requestSeats) { if (requestSeats > 10) { return false; } if (capacity - registeredSeats >= requestSeats) { return true; } return false; } 33
  • 34.
    public class ConferenceRepository { publicint QueryRegisteredSeats(int conferenceId) { using (var dbContext = new ConferenceDbContext()) { IQueryable<int> query = from registration in dbContext.ConferenceRegistrations where registration.ConferenceId == conferenceId select registration.Seats; return query.Sum(); } } }
  • 35.
    public class ConferenceRegistration { publicstring Email { get; set; } public string Name { get; set; } public int ConferenceId { get; set; } public int Seats { get; set; } public bool CanBeRegistered(int capacity, int registeredSeats, int requestSeats) { if (requestSeats > 10) { return false; } if (capacity - registeredSeats >= requestSeats) { return true; } return false; } }
  • 36.
    public class ConferenceRegistration { publicstring Email { get; set; } public string Name { get; set; } public int ConferenceId { get; set; } public int Seats { get; set; } public bool CanBeRegistered(int capacity, int registeredSeats) { if (requestSeats > 10) { return false; } if (capacity - registeredSeats >= requestSeats) { return true; } return false; } }
  • 37.
    public class ConferenceRegistration { publicstring Email { get; set; } public string Name { get; set; } public int ConferenceId { get; set; } public int Seats { get; set; } public bool CanBeRegistered(int capacity, int registeredSeats) { if (this.Seats > 10) { return false; } if (capacity - registeredSeats >= requestSeats) { return true; } return false; } }
  • 38.
    public class ConferenceRegistration { publicstring Email { get; set; } public string Name { get; set; } public int ConferenceId { get; set; } public int Seats { get; set; } public bool CanBeRegistered(int capacity, int registeredSeats) { if (this.Seats > 10) { return false; } if (capacity - registeredSeats >= this.Seats) { return true; } return false; } }
  • 39.
    정리 • 테스트하기 쉬운코드란? • 항상 같은 결과 반환 • 외부상태를 변경하지 않음 • 테스트하기 쉬운 코드로 개발하기 • 방법1: 테스트하기 쉬운코드와 어려운 코드 분리 • 방법2: 두 부류의 코드는 어디서 만나야 하나? • 방법3: 39
  • 40.
  • 41.
    컨퍼런스 등록하기 단계 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 3. 요청한 좌석 수가 확보 가능한지 판단 4. 등록 정보 저장 5. HTTP 결과 반환 41
  • 42.
    컨퍼런스 등록하기 단계 1.ConferenceRegistration 유효성 검사 42
  • 43.
    [Fact] public void 이메일에_골뱅이가_없으면_유효하지_않은_형식입니다() { //Arrange var sut = new ConferenceRegistration { Email = "jwchung_hotmail.com" }; // Act string actual = sut.Validate(); // Assert Assert.NotNull(actual); } 43
  • 44.
    public class ConferenceRegistration { publicstring Email { get; set; } public string Name { get; set; } public int ConferenceId { get; set; } public int Seats { get; set; } public string Validate() { return "이메일은 @를 포함해야 합니다."; } } ? 44
  • 45.
  • 46.
    [Fact] public void 이메일에_골뱅이가_있으면_유효한_형식입니다() { //Arrange var sut = new ConferenceRegistration { Email = "jwchung@hotmail.com" }; // Act string actual = sut.Validate(); // Assert Assert.Null(actual); } 46
  • 47.
    public class ConferenceRegistration { publicstring Email { get; set; } public string Name { get; set; } public int ConferenceId { get; set; } public int Seats { get; set; } public string Validate() { if (this.Email.Contains("@")) return null; return "이메일은 @를 포함해야 합니다."; } } 47
  • 48.
    더 많은 테스트케이스는생략하겠습니다. • 이메일 도메인 파트에 닷(.)이 들어가야합니다. • 이름은 숫자가 포함되면 안됩니다. • 요청 좌석수는 양수여야 합니다. • 컨퍼런스 아이디도 양수입니다. • … 48
  • 49.
    컨퍼런스 등록하기 단계 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 49
  • 50.
    [Fact] public void 컨퍼런스Id로_기_등록된_좌석_수를_조회할_수_있습니다() { //Arrange using (var context = new ConferenceDbContext()) { context.ConferenceRegistrations.Add( new ConferenceRegistration { Email = "foo@bar.com", Name = "foo", ConferenceId = 2, Seats = 2 }); context.ConferenceRegistrations.Add( new ConferenceRegistration { Email = "bar@baz.com", Name = "bar" ConferenceId = 13, Seats = 3 }); context.SaveChanges(); } var sut = new ConferenceRepository(); ... 50
  • 51.
    //using {... context.ConferenceRegistrations.Add( new ConferenceRegistration { Email= "bar@baz.com", Name = "bar" ConferenceId = 13, Seats = 3 }); context.SaveChanges(); } var sut = new ConferenceRepository(); // Act int actual = sut.QueryRegisteredSeats(13); // Assert Assert.Equal(3, actual); } 51
  • 52.
    public class ConferenceRepository { publicint QueryRegisteredSeats(int conferenceId) { return 3; } } 52
  • 53.
    더 많은 조합을테스트하면… // Arrange using (var context = new ConferenceDbContext()) { context.ConferenceRegistrations.Add( new ConferenceRegistration { Email = "foo@bar.com", Name = "foo", ConferenceId = 2, Seats = 2 }); context.ConferenceRegistrations.Add( new ConferenceRegistration { Email = "bar@baz.com", Name = "bar" ConferenceId = 13, Seats = 3 }); context.SaveChanges(); } 53
  • 54.
    public class ConferenceRepository { publicint QueryRegisteredSeats(int conferenceId) { using (var dbContext = new ConferenceDbContext()) { IQueryable<int> query = from registration in dbContext.ConferenceRegistrations where registration.ConferenceId == conferenceId select registration.Seats; return query.Sum(); } } } 54
  • 55.
    컨퍼런스 등록하기 단계 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 55
  • 56.
    컨퍼런스 등록하기 단계 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 56
  • 57.
    컨퍼런스 등록하기 단계 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 3. 요청한 좌석 수가 확보 가능한지 판단 4. 등록 정보 저장 5. HTTP 결과 반환 57
  • 58.
    컨퍼런스 등록하기 단계 1.ConferenceRegistration 유효성 검사 2. 이미 등록된 좌석 수 DB에서 읽어오기 3. 요청한 좌석 수가 확보 가능한지 판단 4. 등록 정보 저장 5. HTTP 결과 반환 58
  • 59.
    59 public class ConferenceRepository: IConferenceRepository { public int QueryRegisteredSeats(int conferenceId) { ... } public void Save(ConferenceRegistration registration) { ... } } public class ConferenceRegistration { public string Email { get; set; } public string Name { get; set; } public int ConferenceId { get; set; } public int Seats { get; set; } public string Validate() { ... } public bool CanBeRegistered(int capacity, int registeredSeats) { ... } }
  • 60.
    정리 • 테스트하기 쉬운코드란? • 항상 같은 결과 반환 • 외부상태를 변경하지 않음 • 테스트하기 쉬운 코드로 개발하기 • 방법1: 테스트하기 쉬운코드와 어려운 코드 분리 • 방법2: 두 부류의 코드는 어디서 만나야 하나? • 방법3: 60
  • 61.
    두 부류의 코드는어디서 만나야 하나? 61
  • 62.
    두 부류의 코드는어디서 만나야 하나? 62
  • 63.
    두 부류의 코드는어디서 만나야 하나? 63
  • 64.
    public bool CanBeRegistered(intcapacity, int conferenceId, int requestSeats) { if (requestSeats > 10) { return false; } using (var dbContext = new ConferenceDbContext()) { IQueryable<int> query = from registration in dbContext.ConferenceRegistrations where registration.ConferenceId == conferenceId select registration.Seats; int registeredSeats = query.Sum(); if (capacity - registeredSeats >= requestSeats) { return true; } return false; } } 64
  • 65.
    두 부류의 코드는어디서 만나야 하나? 65
  • 66.
    두 부류의 코드는어디서 만나야 하나? 66
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
    72 public class ConferenceRegistrationsController: ControllerBase { public ActionResult Post(ConferenceRegistration registration) { string error = registration.Validate(); if (error != null) return this.BadRequest(error); int conferenceId = registration.ConferenceId; var repository = new ConferenceRepository(); int registeredSeats = repository.QueryRegisteredSeats(conferenceId); int capacity = 300; bool canBeRegistered = registration.CanBeRegistered(capacity, registeredSeats); if (!canBeRegistered) return this.BadRequest("잔여석이 없거나, 10좌석을 초과 신청한 경우입니다."); repository.Save(registration); return this.Ok("신청되셨습니다."); } }
  • 73.
    정리3 • 테스트하기 쉬운코드란? • TDD 맛보기 • 테스트하기 쉬운 코드로 개발하기 • 방법1: 테스트하기 쉬운코드와 어려운 코드 분리 • 방법2: 두 부류의 코드는 최대한 가장 자리에 위치 • 방법3: 두 부류 코드가 만나는 가장자리는 어떻게 테스트하나? 73 (예외: 로깅, 퍼사드 )
  • 74.
    가장자리는 어떻게 테스트하는가? •수동테스트 • 자동테스트 74
  • 75.
    수동테스트 75 $ curl -XPOST http://okkycon.com/api/conference-registrations/ ... select top 10 * from ConferenceRegistrations where ConferenceId = 13
  • 76.
  • 77.
    77 [Fact] public void 잔여석이_남은_컨퍼런스는_등록이_가능합니다() { varsut = new ConferenceRegistrationsController(); var registration = new ConferenceRegistration { Email = "jwchung@hotmail.com", Name = "Jin-Wook Chung", ConferenceId = 13, Seats = 2 }; ActionResult actual = sut.Post(registration); Assert.IsType<OkObjectResult>(actual); using (var context = new ConferenceDbContext()) { var query = from r in context.ConferenceRegistrations where r.Email == registration.Email && r.ConferenceId == registration.ConferenceId select r.Seats; var actualSeats = query.First(); Assert.Equal(2, actualSeats); } }
  • 78.
    78 public ActionResult Post(ConferenceRegistrationregistration) { string error = registration.Validate(); if (error != null) return this.BadRequest(error); int conferenceId = registration.ConferenceId; var repository = new ConferenceRepository(); int registeredSeats = repository.QueryRegisteredSeats(conferenceId); int capacity = 300; bool canBeRegistered = registration.CanBeRegistered(capacity, registeredSeats); if (!canBeRegistered) return this.BadRequest("잔여석이 없거나, 10좌석을 초과 신청한 경우입니다."); repository.Save(registration); return this.Ok("신청되셨습니다."); }
  • 79.
    79 public ActionResult Post(ConferenceRegistrationregistration) { var repository = new ConferenceRepository(); repository.Save(registration); return this.Ok("신청되셨습니다."); }
  • 80.
    문제는… 80 public ActionResult Post(ConferenceRegistrationregistration) { using (var context = new ConferenceDbContext()) { context.Database.ExecuteSqlCommand( "insert into ConferenceRegistrations values({0},{1},{2},{3})", registration.Email, registration.Name, registration.ConferenceId, registration.Seats); } return this.Ok("신청되셨습니다."); }
  • 81.
    작성된 코드 사용을강제할 수 있나? • 현 상태의 코드(디자인)로는 할 수 없다. • 실제 클래스 대신 목(Mock) 사용을 위해 이음새(Seam)가 있어야 한다. 81
  • 82.
    이음새(Seam) 도입 82 public interfaceIConferenceRepository { int QueryRegisteredSeats(int conferenceId); void Save(ConferenceRegistration registration); }
  • 83.
    이음새(Seam) 도입 83 public classConferenceRegistrationsController : ControllerBase { public ConferenceRegistrationsController(IConferenceRepository repository) { this.Repository = repository; } public IConferenceRepository Repository { get; } public ActionResult Post(ConferenceRegistration registration) { ... } }
  • 84.
    이음새(Seam) 도입 84 public classConferenceRepository : IConferenceRepository { public int QueryRegisteredSeats(int conferenceId) { ... } public void Save(ConferenceRegistration registration) { ... } }
  • 85.
    수동 목(Mock) 작성 85 publicclass MockRepository : IConferenceRepository { public int ConferenceId { get; set; } public int RegisteredSeats { get; set; } public ConferenceRegistration Registration { get; set; } public int QueryRegisteredSeats(int conferenceId) { this.ConferenceId = conferenceId; return RegisteredSeats; } public void Save(ConferenceRegistration registration) { this.Registration = registration; } }
  • 86.
    86 [Fact] public void 목을_사용해도_잔여석이_남은_컨퍼런스는_등록이_가능합니다() { varmock = new MockRepository { RegisteredSeats = 298 }; var sut = new ConferenceRegistrationsController(mock); var registration = new ConferenceRegistration { Email = "jwchung@hotmail.com", Name = "Jin-Wook Chung", ConferenceId = 13, Seats = 2 }; ActionResult actual = sut.Post(registration); Assert.IsType<OkObjectResult>(actual); Assert.Equal(mock.Registration, registration); Assert.Equal(mock.ConferenceId, registration.ConferenceId); }
  • 87.
    87 public class MockRepository: IConferenceRepository { public int ConferenceId { get; set; } public int RegisteredSeats { get; set; } public ConferenceRegistration Registration { get; set; } public int QueryRegisteredSeats(int conferenceId) { this.ConferenceId = conferenceId; return RegisteredSeats; } public void Save(ConferenceRegistration registration) { this.Registration = registration; } }
  • 88.
    88 [Fact] public void 목을_사용해도_잔여석이_남은_컨퍼런스는_등록이_가능합니다() { varmock = new MockRepository { RegisteredSeats = 298 }; var sut = new ConferenceRegistrationsController(mock); var registration = new ConferenceRegistration { Email = "jwchung@hotmail.com", Name = "Jin-Wook Chung", ConferenceId = 13, Seats = 2 }; ActionResult actual = sut.Post(registration); Assert.IsType<OkObjectResult>(actual); Assert.Equal(mock.Registration, registration); Assert.Equal(mock.ConferenceId, registration.ConferenceId); }
  • 89.
    89 public class MockRepository: IConferenceRepository { public int ConferenceId { get; set; } public int RegisteredSeats { get; set; } public ConferenceRegistration Registration { get; set; } public int QueryRegisteredSeats(int conferenceId) { this.ConferenceId = conferenceId; return RegisteredSeats; } public void Save(ConferenceRegistration registration) { this.Registration = registration; } }
  • 90.
    90 [Fact] public void 목을_사용해도_잔여석이_남은_컨퍼런스는_등록이_가능합니다() { varmock = new MockRepository { RegisteredSeats = 298 }; var sut = new ConferenceRegistrationsController(mock); var registration = new ConferenceRegistration { Email = "jwchung@hotmail.com", Name = "Jin-Wook Chung", ConferenceId = 13, Seats = 2 }; ActionResult actual = sut.Post(registration); Assert.IsType<OkObjectResult>(actual); Assert.Equal(mock.Registration, registration); Assert.Equal(mock.ConferenceId, registration.ConferenceId); }
  • 91.
    91 public class MockRepository: IConferenceRepository { public int ConferenceId { get; set; } public int RegisteredSeats { get; set; } public ConferenceRegistration Registration { get; set; } public int QueryRegisteredSeats(int conferenceId) { this.ConferenceId = conferenceId; return RegisteredSeats; } public void Save(ConferenceRegistration registration) { this.Registration = registration; } }
  • 92.
    92 public IConferenceRepository Repository{ get; } public ActionResult Post(ConferenceRegistration registration) { string error = registration.Validate(); if (error != null) return this.BadRequest(error); int conferenceId = registration.ConferenceId; var repository = new ConferenceRepository(); int registeredSeats = repository .QueryRegisteredSeats(conferenceId); int capacity = 300; bool canBeRegistered = registration.CanBeRegistered(capacity, registeredSeats); if (!canBeRegistered) return this.BadRequest("잔여석이 없거나, 10좌석을 초과 신청한 경우입니다."); repository.Save(registration); return this.Ok("신청되셨습니다."); }
  • 93.
    93 public IConferenceRepository Repository{ get; } public ActionResult Post(ConferenceRegistration registration) { string error = registration.Validate(); if (error != null) return this.BadRequest(error); int conferenceId = registration.ConferenceId; int registeredSeats = this.Repository .QueryRegisteredSeats(conferenceId); int capacity = 300; bool canBeRegistered = registration.CanBeRegistered(capacity, registeredSeats); if (!canBeRegistered) return this.BadRequest("잔여석이 없거나, 10좌석을 초과 신청한 경우입니다."); this.Repository.Save(registration); return this.Ok("신청되셨습니다."); }
  • 94.
    목(Mock) 사용 • 작성된코드 사용을 강제할 수 있다. • 목 사용이 큰 장점으로 보이지만, 생각해볼 점이 많다. 94
  • 95.
    95 행위 검증 행위가 호출되었는가 Mockist 불필요한 추상화 유발가능성 상태 검증 결과 값이 무엇인가 Classicist 불필요한 추상화 필요 없음
  • 96.
    96 행위 검증 구현되지 않는 코드에의존 가능 Outside-in 접근법 클라이언트 시각과 같은 방향 상태 검증 구현된 코드에만 의존 Inside-out 접근법 클라이언트 시각과 반대방향
  • 97.
  • 98.
    제가 생각하는 목(Mock)사용 문제점 • 목을 남발할 가능성이 크다. • 대부분 목 사용 예제는 간단하다. 그래서 장점이 크게 보인다. • 실제 프로젝트에 적용하면 한꺼번에 많은 수의 목을 다루면서 곤란을 겪는다. • 적당 수의 목 사용에 대한 답을 찾기 어렵다. • 상태 검증으로 돌아가보자. 98
  • 99.
    99 [Fact] public void 잔여석이_남은_컨퍼런스는_등록이_가능합니다() { varsut = new ConferenceRegistrationsController(); var registration = new ConferenceRegistration { Email = "jwchung@hotmail.com", Name = "Jin-Wook Chung", ConferenceId = 13, Seats = 2 }; ActionResult actual = sut.Post(registration); Assert.IsType<OkObjectResult>(actual); using (var context = new ConferenceDbContext()) { var query = from r in context.ConferenceRegistrations where r.Email == registration.Email && r.ConferenceId == registration.ConferenceId select r.Seats; var actualSeats = query.First(); Assert.Equal(2, actualSeats); } }
  • 100.
    100 public ActionResult Post(ConferenceRegistrationregistration) { using (var context = new ConferenceDbContext()) { context.Database.ExecuteSqlCommand( "insert into ConferenceRegistrations values({0},{1},{2},{3})", registration.Email, registration.Name, registration.ConferenceId, registration.Seats); } return this.Ok("신청되셨습니다."); }
  • 101.
    상태 검증 -문제 극복 방안 • TDD를 통한 사전이 아니라 사후 테스트를 하자. • 두 부류의 코드가 맞물려 잘 돌아가는 로직이다. • 난해한 코드가 아니다. • 구현된 코드를 사용하지 않고 굳이 어려운 길을 택할 이유가 없다. • 완벽을 추구하면서 목을 사용하는 비용을 들일 필요가 있는가 101
  • 102.
    정리4(최종) • 테스트하기 쉬운코드란? • 테스트하기 쉬운 코드로 개발하기 • 방법1: 테스트하기 쉬운코드와 어려운 코드 분리 • 방법2: 두 부류의 코드는 최대한 가장 자리에 위치 • 방법3: 가장 자리를 테스트하는 방법을 익히자 • 수동 • 자동: 상태검증 / 행위검증 102
  • 103.
    끝으로 • 요즘 저는 •두 부류 코드를 분리해서 각각 테스트하고, • 가장자리에서 맞물려 돌아가는 코드는 주로 수동테스트합니다. • 두 부류 코드 섞어 넣고 테스트가 어렵다고 포기하지 마세요. • 위 내용과 관련된 제 블로그 글이 있답니다. • http://jwchung.github.io/testing-oh-my 103
  • 104.