[W-2] Service Architecture and Test Code

YC Tech Academy Backend Career Project

Overview

This week, our main topic of lecture was beans and dependency injection. Additionally, by using these, we have learned to create test programs for our services and repository functions.

Most topics were already studied in previous blog, so for this blog, I will mostly be mentioning new topics taught during the lecture, and some topics will only be briefly mentioned.

Preview from W1

Before we moved on, we reviewed and finished projects from last week, which was establishing the post system.

DTO (Data Transfer Objects)

To develop our post system, we introduced DTOs (Data Transfer Objects), specifically the PostDTO and ResultDTO. The PostDTO encapsulates the core data of the post, such as its title and contents. On the other hand, the ResultDTO represents the actual object dispatched during API calls.

Notably, both these DTOs utilize the record interface, ensuring immutability. This attribute enhances data security during transfers. Subsequently, with the advent of these new DTOs, we revamped the PostController. Now, it returns a ResultDTO that houses the PostDTO.

Domain (VO)

In one of my recent lectures, I delved into the intriguing world of software design and stumbled upon the term "domain," specifically in the context of Domain-Driven Design (DDD).

Now, when we speak of "domain" in this context, we're referring to the specific area our software is designed to operate within. Within this vast domain, there are various design patterns, and one such pattern that caught my attention was the Value Object (VO). A VO is an intriguing concept; it describes specific attributes of our domain without attaching any unique identity to them.

For instance, in our discussion, we created a VO for a "post." This VO provides a consistent and structured way to represent the details of a post, ensuring that when our API communicates with our database, everything remains cohesive and precise. It's a neat way to maintain clarity and integrity in data representation, and while the naming choice of "domain" might seem unconventional at first glance, it underscores the importance of understanding the foundational concepts in our software's architecture.

For example, there is an API can calls for an specific post by its ID. The following is the code for the function:

@GetMapping("/{id}")
public ResultDto<PostDto> getPostWithId(
        @Parameter(description = "Post ID")
        @PathVariable String id) {
    Post post = postService.getPost(Long.parseLong(id));
    PostDto postDto = new PostDto(post.getTitle(), post.getContent());
    return ResultDto.success(postDto);
}

First, the post is called form the @Service object. Then the post is transformed into a DTO, then the DTO is sent with a ResultDTO.

@Service and @Repository

From the provided code, we can observe that the @Service annotation is essential for its proper operation. Two related classes, PostService and PostServiceImpl, are introduced, the significance of which will become apparent later.

The following is the code for PostService

@Component
public interface PostService {
        Post addPost(String title, String content);
        Post getPost(Long id);
        List<Post> getPostListByUserId();
        List<Post> getPostList();
}

The @Component annotation designates it as a bean, making it eligible for dependency injection. Being an interface, PostService can have multiple concrete implementations, allowing developers flexibility in defining method behaviors.

Subsequently, we have the PostRepository:

public interface PostRepository extends JpaRepository<Post, Serializable> {
    List<Post> findByOrderByIdDesc();
    List<Post> findAllByUserId(Long userId);
    List<Post> findByIdInOrderByIdDesc(List<Long> postIdList);
}

Similar to PostService, PostRepository is an interface, but it derives from the JpaRepository interface. This inheritance equips it with foundational operations essential for data retrieval and manipulation. At this juncture, it's important to note that the database isn't fully configured, so the repository remains unpopulated.

Beans

After all of this is set up, the lecture goes on talking about beans and its significance. While most topics were mentioned in the previous blog, there are some details that the lecturer added.

Bean LifeStyle Callback

The following is the code showing bean's callbacks

public class ExampleBean {
 @PostConstruct
 public void initialize() throws Exception {
 // 초기화 콜백 (의존관계 주입이 끝나면 호출)
 }
 @PreDestroy
 public void close() throws Exception {
 // 소멸 전 콜백 (메모리 반납, 연결 종료와 같은 과정)
 }
}

@PostConstruct and @PreDestroy both have self-explanatory names, and these callbacks will be frequently used when developing applications.

Bean Scope

There are two types of scopes for beans: Singleton and Prototype.

The Singleton Scope in Bean Management

In the realm of Spring framework's bean management, the Singleton scope stands out as the default and most commonly used scope. When a bean is defined as a Singleton, it signifies that the Spring container will instantiate only one instance of that bean. This single instance is then cached within the container, ensuring that every request for that bean will receive the same, shared instance.

The main advantage of the Singleton scope is the efficiency of resource utilization, as it avoids repeated creation of an object. However, developers should be cautious when managing state within a Singleton bean, as its shared nature means that state changes can affect all components that reference it.

The Prototype Scope: A Contrast to Singleton

On the other side of the spectrum lies the Prototype scope. Contrary to Singleton, when a bean is set to the Prototype scope, the Spring container creates a new instance of the bean every time there is a request for that bean. This ensures that each component or service requesting the bean receives its own dedicated instance, thus eliminating shared state concerns.

Prototype is especially useful in situations where bean state management is critical or when the bean has a short-lived nature and needs to be garbage collected after its purpose is served. Choosing between Singleton and Prototype scopes often boils down to the specific needs of the application and its performance and memory considerations.

Test functions

The main advantage of using beans and dependency injection is being able to make quick test fuctions for each features. For our SNS tutorial application, we made some test functions for controller and service.

Controller Test

public class PostControllerTest {

    private MockMvc mockMvc;

    ObjectMapper mapper = new ObjectMapper();

    final String TITLE = "Title Test";
    final String CONTENT = "Content Test";

    @Test
    public void getPostTest() throws Exception {
        this.mockMvc.perform(
                get("/post/{postId}", "1"))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("data")))
                .andExpect(jsonPath("$.data").exists())
                .andDo(print());

    }
}

The PostControllerTest class is a unit test class for testing a specific endpoint (in this case, a "post" endpoint) within a web application using the Spring framework. This test focuses on ensuring the behavior of retrieving a post based on its postId. Let's break down the various components and their purposes:

  1. MockMvc mockMvc:

    • MockMvc is a part of Spring Test library, and it provides a powerful way to test Spring MVC applications without having to start an actual web server or send actual HTTP requests.

    • The mockMvc instance is used to mock web requests and responses, making it suitable for testing the web layer without affecting the actual server state.

  2. ObjectMapper mapper:

    • ObjectMapper is a part of the Jackson library. It provides functionality for reading and writing JSON.

    • In this context, it seems to be set up for potential future use. It can be used for converting objects to JSON format (serialization) or converting JSON to objects (deserialization). However, in this specific test method (getPostTest), the mapper is not being used.

  3. final String TITLE and final String CONTENT:

    • These are constant string values (final denotes they cannot be changed after initialization).

    • They appear to be set up for potential future use. They could be used in tests where we might want to create a new post or verify the content of a post, but they're not utilized in the getPostTest method.

  4. @Test Annotation:

    • The @Test annotation is from the JUnit testing framework and indicates that the getPostTest method is a test case that should be executed by the test runner.
  5. getPostTest() Method:

    • This method is the actual test case.

    • this.mockMvc.perform(...): This initiates an HTTP request using MockMvc.

      • get("/post/{postId}", "1"): It sends an HTTP GET request to the /post/1 URL (retrieving a post with the postId of "1").
    • .andExpect(status().isOk()): Asserts that the response's HTTP status code is 200 OK.

    • .andExpect(content().string(containsString("data"))): Checks that the response body contains the string "data".

    • .andExpect(jsonPath("$.data").exists()): Validates that the response body contains a JSON field named "data" at the root level.

    • .andDo(print()): This is a result handler that prints the result details to the console. It's useful for debugging and understanding the response during test development.

Service Test

@ExtendWith(MockitoExtension.class)
public class PostServiceTest {

    private PostService postService;

    @Mock
    private PostRepository postRepository;

    @BeforeEach
    public void init() {
        postService = new PostServiceImpl(postRepository);
    }

    @Test
    @DisplayName("add 시 repository 가 호출되는 지 확인")
    public void test_post_add() {
        postService.addPost("test title", "test content");
        verify(postRepository, atLeastOnce()).save(any());
    }

    @Test
    @DisplayName("전체 post 조회")
    public void test_get_posts() {
        Post post1 = new Post(LocalDate.now().minusDays(2L));
        Post post2 = new Post(LocalDate.now().plusDays(2L));
        List<Post> stubPosts = List.of(post1, post2);

        // stubbing
        when(postRepository.findAll()).thenReturn(stubPosts);

        List<Post> posts = postService.getPostList();

        assertTrue(isSortedDescending(posts));

    }

    private boolean isSortedDescending(List<Post> items) {
        return IntStream.range(0, items.size() - 1)
                .noneMatch(i -> items.get(i).getCreatedAt().isBefore(items.get(i + 1).getCreatedAt()));
    }


}

PostServiceTest is a unit test class for the PostService. It uses Mockito to mock the PostRepository and tests if the service correctly interacts with the repository and ensures the correctness of its methods, specifically for adding a post and fetching all posts in a sorted order.

  1. @ExtendWith(MockitoExtension.class):

    • Integrates Mockito with JUnit 5. This allows for using Mockito's annotations and capabilities seamlessly within a JUnit 5 test.
  2. private PostService postService:

    • An instance of the service under test.
  3. @Mock private PostRepository postRepository:

    • Mockito's @Mock annotation creates a mock instance of PostRepository. This allows simulating the behavior of the repository without interacting with the actual database.
  4. @BeforeEach public void init():

    • This method runs before each test. It initializes the postService with the mock repository.
  5. @Test public void test_post_add():

    • Tests the behavior of adding a post.

    • After adding a post, it verifies if the repository's save method was called at least once.

  6. @Test public void test_get_posts():

    • This test checks the behavior of fetching a list of posts.

    • Two stub posts are created. Mockito's when is used to return these stub posts when the repository's findAll method is called.

    • The retrieved posts are then checked to ensure they are sorted in descending order based on creation date.

  7. private boolean isSortedDescending(List<Post> items):

    • Helper method that checks if a list of posts is sorted in descending order by their creation date.

Stubbing

From the PostServiceTest class, we observed an instance of stubbing in the test_get_posts method. Stubbing is an essential aspect of mocking, where you pre-program the behavior or return value of a method for the purpose of the test.

When we look at the line when(postRepository.findAll()).thenReturn(stubPosts);, this is an example of stubbing. Here, the behavior of the findAll method of the postRepository mock object is being controlled. Instead of executing the real method, when findAll is called, it will simply return the stubPosts list.

Significance of Stubbing:

  1. Isolation of Components: Stubbing allows a test to isolate the component under test (in this case, PostService) from its dependencies (like PostRepository). This ensures that the test only verifies the logic of the component and isn't affected by the actual behavior or any potential errors in the dependencies.

  2. Predictable Behavior: By stubbing methods, we can simulate various scenarios, including corner cases, without having to set up a real environment to reproduce those scenarios. This ensures tests are consistent and reproducible.

  3. Speed: Real implementations, especially ones that involve IO operations like database calls, can be slow. Stubbing can drastically speed up the execution of tests.

  4. Simulate Exception Scenarios: Apart from returning predefined results, stubbing can also be used to throw exceptions. This is useful for testing how the system under test behaves under error conditions without having to artificially cause these errors in real dependencies.

  5. Focus on Business Logic: When testing services or business logic, you often don't want to be bothered by the actual data storage or retrieval mechanisms. Stubbing allows you to bypass this and focus solely on ensuring that the business logic behaves correctly.

Conclusion

This week's focus on beans and dependency injection greatly enriched our understanding of how to better structure and test our applications. We delved into Data Transfer Objects (DTOs), learning how PostDTO and ResultDTO streamline and secure data transfers.

The world of Domain-Driven Design introduced us to the concept of domains and the significance of Value Objects. Through code snippets, we explored various aspects like the @Service annotation, the role of @Repository, and the nuances of beans, particularly in relation to their lifecycle and scope.

Testing emerged as a crucial topic, emphasizing the benefits of dependency injection, where we learned to employ MockMvc for controllers and Mockito for services. Stubbing stood out, offering an efficient way to simulate behaviors, ensuring our tests remain predictable and focused on the business logic.

All in all, these lessons not only fortify our foundational knowledge but pave the way for creating more robust and maintainable applications in the future.

Github Code Reference

Github link