다음으로 작성할 테스트 코드는 서비스 테스트 코드이다. [[4-0. 리포지터리 테스트 코드|이전 리포지터리 테스트 코드]]와 유사하게 테스트 코드를 먼저 작성한 다음 실제 서비스 코드를 작성한다.
## `config` 작성
엔티티와 DTO 변환은 컨트롤러에서 할 생각이라 서비스는 엔티티만 다룬다. 따라서 따로 Config를 작성할 필요 없이 [[4-0. 리포지터리 테스트 코드#`PromptTemplateTestConfig`|이전 테스트에서 작성한 Config]]를 그대로 사용하면 된다.
![[4-0. 리포지터리 테스트 코드#`PromptTemplateTestConfig`]]
## `PromptTemplateServiceTest` 작성
### 클래스 생성
```java
@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
@Import(PromptTemplateTestConfig.class)
public class PromptTemplateServiceTest {
}
```
- 테스트에서 많이 사용하는 `@Transactional`은 사용하지 않는다. H2의 휘발성 때문에 설정하지 않아도 테스트 결과가 다 지워지기 때문이다.
### 의존성 주입
```java
@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
@Import(PromptTemplateTestConfig.class)
public class PromptTemplateServiceTest {
@Autowired
private PromptTemplateService service;
@Autowired
private SingleArgAssertor<PromptTemplate> assertNotNull;
@Autowired
private DoubleArgAssertor<PromptTemplate, PromptTemplate> assertEquality;
}
```
### 메서드 작성
#### `service` 주입 여부 확인
```java
@Test
public void testServiceExistence(){
assertNotNull(service);
}
```
#### CRUD 테스트
```java
@Test
public void testCreate(){
//Given
PromptTemplate promptTemplate = PromptTemplate.builder()
.id("test_instance")
.content("Say ${word} twice!")
.modifiedTime(LocalDateTime.now())
.build();
//When
final PromptTemplate savedTemplate = service.create(promptTemplate);
//Then
notNullAssertor.assertify(savedTemplate);
equalityAssertor.assertify(promptTemplate, savedTemplate);
}
```
```java
@Test
public void testReadById(){
//Given
PromptTemplate promptTemplate = PromptTemplate.builder()
.id("test_instance2")
.content("What is ${word}?")
.modifiedTime(LocalDateTime.now())
.build();
//When
service.create(promptTemplate);
final PromptTemplate readTemplate = service.readById("test_instance2");
//Then
notNullAssertor.assertify(readTemplate);
equalityAssertor.assertify(promptTemplate, readTemplate);
assertThrows(
EntityNotFoundException.class,
()->service.readById("I am really Id. Trust me!")
);
}
```
- 이번 예시 프로젝트에서 예외 처리는 `@ControllerAdvice`에서 전부 수행한다. 따라서 `PromptTemplateService`는 예외 처리를 하지 않고 엔티티가 없으면 `EntityNotFoundException`를 throw 하게 할 생각이다.
```java
@Test
public void testUpdateById(){
//Given
PromptTemplate promptTemplate = PromptTemplate.builder()
.id("test_instance3")
.content("")
.modifiedTime(LocalDateTime.now())
.build();
PromptTemplate modifiedTemplate = PromptTemplate.builder()
.content("What is the most biggest ${word}?")
.modifiedTime(LocalDateTime.now())
.build();
//When
service.create(promptTemplate);
PromptTemplate readTemplate = service.readById("test_instance3");
final PromptTemplate updatedTemplate = service.updateById("test_instance3", modifiedTemplate);
modifiedTemplate.setId("test_instance3");
//Then
notNullAssertor.assertify(updatedTemplate);
equalityAssertor.assertify(updatedTemplate, modifiedTemplate);
assertThrows(
EntityNotFoundException.class,
()->service.updateById("I am really Id. Trust me!", modifiedTemplate)
);
}
```
```java
@Test
public void testDeleteById(){
//Given
PromptTemplate promptTemplate = PromptTemplate.builder()
.id("test_instance4")
.content("haha")
.modifiedTime(LocalDateTime.now())
.build();
//When
service.create(promptTemplate);
service.deleteById("test_instance4");
//Then
assertThrows(
EntityNotFoundException.class,
()->service.readById("test_instance4")
);
}
```
#### 템플릿을 통한 프롬프트 생성 기능 테스트
ChatGPT 프록시 서버의 [[0. ChatGPT 프록시 서버 소개 및 주요 서비스#주요 서비스|주요 서비스]] 중 **프롬프트 템플릿을 통한 프롬프트 생성**은 `PromptTemplateService`에서 구현되어야 한다. 그러니 템플릿을 통한 프롬프트 생성 서비스를 테스트하는 코드를 작성해보자.
```java
@Test
public void testGeneratePromptByTemplate() throws IOException, TemplateException {
//Given
String templateContent = Files.readString(Paths.get("src/test/resources/templates/example1.template"));
String reference = Files.readString(Paths.get("src/test/resources/templates/example1.reference"));
String input = Files.readString(Paths.get("src/test/resources/templates/example1.input"));
String answer = Files.readString(Paths.get("src/test/resources/templates/example1.answer"));
Map<String, String> map = new HashMap<>();
map.put("reference", reference);
map.put("input", input);
PromptTemplate promptTemplate = PromptTemplate.builder()
.id("test_instance5")
.content(templateContent)
.modifiedTime(LocalDateTime.now())
.build();
//When
service.create(promptTemplate);
String prompt = service.generatePromptByTemplate("test_instance5", map);
//Then
assertEquals(prompt, answer);
}
```
- 프롬프트 템플릿과 키워드들의 길이가 너무 길어서 따로 파일로 저장한 다음 불러오도록 설정했다. 각 파일의 내용은 [[4-1. 서비스 테스트 코드#프롬프트 템플릿 예시 파일|맨 마지막]]에 적어두었다. 다만, 파일의 정확한 내용은 중요하지 않으니 굳이 보지 않아도 된다.
- 중요한 건 `generatePromptByTemplate` 메서드가 템플릿과 그 템플릿에 들어갈 키워드를 받아 템플릿 안에 집어 넣은 뒤 반환한다는 것이다.
- 템플릿에서 키워드는 `${키워드}` 의 형식으로 표시된다. 그 외의 반복문을 위한 표시 방법 같은 경우는 [여기](https://freemarker.apache.org/docs/dgui_template_exp.html)를 참조하면 확인 가능하다.
## [[6. Service 구현|서비스 코드 작성]]
![[6. Service 구현#클래스 생성 및 의존성 주입]]
![[6. Service 구현#CRUD 메서드]]
![[6. Service 구현#템플릿을 통한 프롬프트 생성 메서드]]
## 테스트 결과 확인
![[Pasted image 20231213185602.png]]
## 프롬프트 템플릿 예시 파일([[4-1. 서비스 테스트 코드#프롬프트 템플릿을 통한 프롬프트 생성|돌아가기]])
### example1.template
```markdown
# Order
I want you to act like a web server developer. I will give you the code I wrote under the "Reference". And I'll give you a code that includes "[blank]" under the "Input". Then you can refer to the "Reference" code and fill in "[blank]" according to the following constraints.
# Constraints
- only reply with the output inside one unique code block, and nothing else
- do not write explanations
# Reference
${reference}
# Input
${input}
# Output type
[output code]
```
### example1.reference
```java
import lombok.*;
@Getter
@Setter
@Entity
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="post_id", updatable = false)
private int id;
private String content;
private String title;
@CreatedDate
@Column(name = "posted_date")
private LocalDateTime postedDate;
@ManyToOne
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "user_id")
private Users user;
}
```
### example1.input
```java
import lombok.*;
[blank]
public class PostRequestDTO {
[blank]
public [blank] toEntity([blank]){
[blank]
}
}
```
### example1.answer
```markdown
# Order
I want you to act like a web server developer. I will give you the code I wrote under the "Reference". And I'll give you a code that includes "[blank]" under the "Input". Then you can refer to the "Reference" code and fill in "[blank]" according to the following constraints.
# Constraints
- only reply with the output inside one unique code block, and nothing else
- do not write explanations
# Reference
import lombok.*;
@Getter
@Setter
@Entity
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="post_id", updatable = false)
private int id;
private String content;
private String title;
@CreatedDate
@Column(name = "posted_date")
private LocalDateTime postedDate;
@ManyToOne
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "user_id")
private Users user;
}
# Input
import lombok.*;
[blank]
public class PostRequestDTO {
[blank]
public [blank] toEntity([blank]){
[blank]
}
}
# Output type
[output code]
```