Implementing Unit Tests for a Reactive App that uses Spring WebFlux and MongoDB
Step-by-step guide on how to implement Unit tests for Book API using Spring Testing Library
In this article, we will explain how to implement Unit Tests in a Spring Boot Reactive application, whose name is Book API.
You can find the complete code and implementation in the article linked below. Feel free to follow the steps explained in the article and get started.
Unit test is a software testing approach that focuses on isolating and evaluating individual components or units of a software application in isolation.
In the context of the Book API, unit test would involve testing each component, class, or method separately to ensure that they perform their specific functions correctly.
Besides, we will explore the annotations and utilities that Spring Boot provides in order to unit test the applications.
So, let’s get started!
Create some packages
In the src/test/java folder, let’s create the following packages inside the com.example.bookapi root package: controller, mapper, and service.
Disable the BookApiApplicationTests class
Let’s disable the BookApiApplicationTests class that was generated while creating the project using Spring Initializr. We will not implement test cases in it. For it, add the following bold lines:
package com.example.bookapi;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@Disabled
@SpringBootTest
class BookApiApplicationTests {
@Test
void contextLoads() {
}
}Create the BookMapperImplTest class
In the mapper package, create the BookMapperImplTest class with the following content:
package com.example.bookapi.mapper;
import com.example.bookapi.controller.dto.BookResponse;
import com.example.bookapi.controller.dto.CreateBookRequest;
import com.example.bookapi.controller.dto.UpdateBookRequest;
import com.example.bookapi.model.Book;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@Import(BookMapperImpl.class)
class BookMapperImplTest {
@Autowired
private BookMapper bookMapper;
@Test
void testToBook() {
CreateBookRequest createBookRequest = new CreateBookRequest("title", "author", 2023);
Book book = bookMapper.toBook(createBookRequest);
assertThat(book.getId()).isNull();
assertThat(book.getTitle()).isEqualTo("title");
assertThat(book.getAuthor()).isEqualTo("author");
assertThat(book.getYear()).isEqualTo(2023);
}
@ParameterizedTest
@MethodSource("provideUpdateBookRequests")
void testUpdateBookFromUpdateBookRequest(UpdateBookRequest updateupdateBookRequest, Book expectedBook) {
Book book = new Book("title", "author",2023);
bookMapper.updateBookFromUpdateBookRequest(updateupdateBookRequest, book);
assertThat(book.getId()).isEqualTo(expectedBook.getId());
assertThat(book.getTitle()).isEqualTo(expectedBook.getTitle());
assertThat(book.getAuthor()).isEqualTo(expectedBook.getAuthor());
assertThat(book.getYear()).isEqualTo(expectedBook.getYear());
}
private static Stream<Arguments> provideUpdateBookRequests() {
return Stream.of(
Arguments.of(new UpdateBookRequest("newTitle", null, null), new Book("newTitle", "author", 2023)),
Arguments.of(new UpdateBookRequest(null, "newAuthor", null), new Book("title", "newAuthor", 2023)),
Arguments.of(new UpdateBookRequest(null, null, 2024), new Book("title", "author", 2024)),
Arguments.of(new UpdateBookRequest("newTitle", "newAuthor", 2024), new Book("newTitle", "newAuthor", 2024))
);
}
@Test
void testToBookResponse() {
Book book = new Book("title", "author", 2023);
BookResponse bookResponse = bookMapper.toBookResponse(book);
assertThat(bookResponse.id()).isNull();
assertThat(bookResponse.title()).isEqualTo("title");
assertThat(bookResponse.author()).isEqualTo("author");
assertThat(bookResponse.year()).isEqualTo(2023);
}
}This BookMapperImplTest is designed to test the functionality of the BookMapperImpl class, which is responsible for mapping between different representations of a book (DTOs and the entity).
Let's break down the key components of this test class:
Annotations
@ExtendWith(SpringExtension.class): This annotation integrates the Spring TestContext Framework with JUnit 5, allowing the usage of the Spring test features, such as dependency injection of beans;@Import(BookMapperImpl.class): Imports theBookMapperImplclass, indicating that the test will focus on the functionality provided by this specific mapper implementation.
Fields
@Autowired BookMapper bookMapper: Injects an instance of theBookMapperImplclass to test its functionality.
Test Methods
testToBook(): Tests thetoBookmethod, which converts aCreateBookRequestDTO to aBookentity. It checks that the resultingBookhas the expected attributes;testUpdateBookFromUpdateBookRequest(): Parameterized test for theupdateBookFromUpdateBookRequestmethod. It tests the method's ability to update aBookentity based on differentUpdateBookRequestDTOs. The provided arguments include the update request and the expected resultingBook;testToBookResponse(): Tests thetoBookResponsemethod, which converts aBookentity to aBookResponseDTO. It checks that the resultingBookResponsehas the expected attributes.
Create the BookServiceImplTest class
In the service package, create the BookServiceImplTest class with the content below:
package com.example.bookapi.service;
import com.example.bookapi.exception.BookNotFoundException;
import com.example.bookapi.model.Book;
import com.example.bookapi.repository.BookRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
@Import(BookServiceImpl.class)
class BookServiceImplTest {
@Autowired
private BookService bookService;
@MockBean
private BookRepository bookRepository;
@Test
void testGetBooksWhenThereIsNoBook() {
when(bookRepository.findAll()).thenReturn(Flux.empty());
StepVerifier.create(bookService.getBooks())
.expectNextCount(0)
.verifyComplete();
}
@Test
void testGetBooksWhenThereIsOneBook() {
Book book = getDefaultBook();
when(bookRepository.findAll()).thenReturn(Flux.just(book));
StepVerifier.create(bookService.getBooks())
.expectNext(book)
.verifyComplete();
}
@Test
void testValidateAndGetBookByIdWhenExisting() {
Book book = getDefaultBook();
when(bookRepository.findById(anyString())).thenReturn(Mono.just(book));
StepVerifier.create(bookService.validateAndGetBookById("123"))
.consumeNextWith(bookFound -> assertThat(bookFound).isEqualTo(book))
.verifyComplete();
}
@Test
void testValidateAndGetBookByIdWhenNonExisting() {
when(bookRepository.findById(anyString())).thenReturn(Mono.empty());
StepVerifier.create(bookService.validateAndGetBookById("123"))
.verifyErrorMatches(ex -> ex instanceof BookNotFoundException);
}
@Test
void testSaveBook() {
Book book = getDefaultBook();
when(bookRepository.save(any(Book.class))).thenReturn(Mono.just(book));
StepVerifier.create(bookService.saveBook(book))
.consumeNextWith(bookFound -> assertThat(bookFound).isEqualTo(book))
.verifyComplete();
}
@Test
void testDeleteBook() {
Book book = getDefaultBook();
when(bookRepository.delete(any(Book.class))).thenReturn(Mono.empty());
StepVerifier.create(bookService.deleteBook(book))
.verifyComplete();
}
private Book getDefaultBook() {
return new Book("123", "title", "author", 2023);
}
}The BookServiceImplTest is designed to test the functionality of the BookServiceImpl class, which is the implementation of the BookService interface. For each test, it uses StepVerifier to verify the behavior of the reactive streams (Flux and Mono) returned by the service methods. This includes assertions about the emitted values, completion, and potential errors
Let's break down the key components of this test class:
Annotations
@Import(BookServiceImpl.class): Imports theBookServiceImplclass, indicating that the test will focus on the functionality provided by this specific service implementation.
Fields
@Autowired BookService bookService: Injects an instance of theBookServiceImplclass to test its functionality;@MockBean BookRepository bookRepository: Injects a mock instance of theBookRepositoryto simulate database interactions during testing.
Test Methods
testGetBooksWhenThereIsNoBook(): Tests thegetBooksmethod of theBookServicewhen there are no books in the repository. It usesStepVerifierto assert that the resultingFluxis empty;testGetBooksWhenThereIsOneBook(): Tests thegetBooksmethod when there is one book in the repository. It verifies that theFluxemitted the book;testValidateAndGetBookByIdWhenExisting(): Tests thevalidateAndGetBookByIdmethod when a book with a specified ID exists. It checks that the resultingMonoemits the expected book;testValidateAndGetBookByIdWhenNonExisting(): Tests thevalidateAndGetBookByIdmethod when a book with a specified ID does not exist. It checks that the resultingMonoemits aBookNotFoundException;testSaveBook(): Tests thesaveBookmethod by verifying that the resultingMonoemits the saved book;testDeleteBook(): Tests thedeleteBookmethod by verifying that the resultingMonocompletes successfully.
Create the DTO test classes
In the controller package, let’s create a new package called dto. Inside the dto package, we will create the three test classes: CreateBookRequestTest, UpdateBookRequestTest and BookResponseTest:
Let’s start creating the CreateBookRequestTest class with the following content:
package com.example.bookapi.controller.dto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
@JsonTest
class CreateBookRequestTest {
@Autowired
private JacksonTester<CreateBookRequest> jacksonTester;
@Test
void testSerialize() throws IOException {
CreateBookRequest createBookRequest = new CreateBookRequest("title", "author", 2023);
JsonContent<CreateBookRequest> jsonContent = jacksonTester.write(createBookRequest);
assertThat(jsonContent)
.hasJsonPathStringValue("@.title")
.extractingJsonPathStringValue("@.title").isEqualTo("title");
assertThat(jsonContent)
.hasJsonPathStringValue("@.author")
.extractingJsonPathStringValue("@.author").isEqualTo("author");
assertThat(jsonContent)
.hasJsonPathNumberValue("@.year")
.extractingJsonPathNumberValue("@.year").isEqualTo(2023);
}
@Test
void testDeserialize() throws IOException {
String content = "{\"title\":\"title\",\"author\":\"author\",\"year\":2023}";
CreateBookRequest createBookRequest = jacksonTester.parseObject(content);
assertThat(createBookRequest.title()).isEqualTo("title");
assertThat(createBookRequest.year()).isEqualTo(2023);
assertThat(createBookRequest.author()).isEqualTo("author");
}
}Next, we will create the UpdateBookRequestTest class:
package com.example.bookapi.controller.dto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
@JsonTest
class UpdateBookRequestTest {
@Autowired
private JacksonTester<UpdateBookRequest> jacksonTester;
@Test
void testSerialize() throws IOException {
UpdateBookRequest updateBookRequest = new UpdateBookRequest("title", "author", 2023);
JsonContent<UpdateBookRequest> jsonContent = jacksonTester.write(updateBookRequest);
assertThat(jsonContent)
.hasJsonPathStringValue("@.title")
.extractingJsonPathStringValue("@.title").isEqualTo("title");
assertThat(jsonContent)
.hasJsonPathStringValue("@.author")
.extractingJsonPathStringValue("@.author").isEqualTo("author");
assertThat(jsonContent)
.hasJsonPathNumberValue("@.year")
.extractingJsonPathNumberValue("@.year").isEqualTo(2023);
}
@Test
void testDeserialize() throws IOException {
String content = "{\"title\":\"title\",\"author\":\"author\",\"year\":2023}";
UpdateBookRequest updateBookRequest = jacksonTester.parseObject(content);
assertThat(updateBookRequest.title()).isEqualTo("title");
assertThat(updateBookRequest.year()).isEqualTo(2023);
assertThat(updateBookRequest.author()).isEqualTo("author");
}
}Finally, let’s create the BookResponseTest class with the content below:
package com.example.bookapi.controller.dto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
@JsonTest
class BookResponseTest {
@Autowired
private JacksonTester<BookResponse> jacksonTester;
@Test
void testSerialize() throws IOException {
BookResponse bookResponse = new BookResponse("123", "title", "author", 2023);
JsonContent<BookResponse> jsonContent = jacksonTester.write(bookResponse);
assertThat(jsonContent)
.hasJsonPathStringValue("@.id")
.extractingJsonPathStringValue("@.id").isEqualTo("123");
assertThat(jsonContent)
.hasJsonPathStringValue("@.title")
.extractingJsonPathStringValue("@.title").isEqualTo("title");
assertThat(jsonContent)
.hasJsonPathStringValue("@.author")
.extractingJsonPathStringValue("@.author").isEqualTo("author");
assertThat(jsonContent)
.hasJsonPathNumberValue("@.year")
.extractingJsonPathNumberValue("@.year").isEqualTo(2023);
}
@Test
void testDeserialize() throws IOException {
String content = "{\"id\":\"123\",\"title\":\"title\",\"author\":\"author\",\"year\":2023}";
BookResponse bookResponse = jacksonTester.parseObject(content);
assertThat(bookResponse.id()).isEqualTo("123");
assertThat(bookResponse.title()).isEqualTo("title");
assertThat(bookResponse.author()).isEqualTo("author");
assertThat(bookResponse.year()).isEqualTo(2023);
}
}These test classes are employed to verify the correct serialization and deserialization of Data Transfer Object (DTO) classes using Jackson.
The @JsonTest annotation signals that this class is designed for JSON testing, establishing a Spring test environment that incorporates automatic Jackson configuration for JSON serialization and deserialization.
Additionally, the JacksonTester utility, supplied by Spring Boot, facilitates JSON serialization and deserialization testing.
Create the BookControllerTest class
In the controller package, create the BookControllerTest class with the content below:
package com.example.bookapi.controller;
import com.example.bookapi.controller.dto.BookResponse;
import com.example.bookapi.controller.dto.CreateBookRequest;
import com.example.bookapi.controller.dto.UpdateBookRequest;
import com.example.bookapi.exception.BookNotFoundException;
import com.example.bookapi.mapper.BookMapperImpl;
import com.example.bookapi.model.Book;
import com.example.bookapi.service.BookService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
@WebFluxTest(BookController.class)
@Import(BookMapperImpl.class)
class BookControllerTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private BookService bookService;
@Test
void testGetBooksWhenThereIsNone() {
when(bookService.getBooks()).thenReturn(Flux.empty());
webTestClient.get()
.uri(API_BOOKS_URL)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_NDJSON_VALUE)
.expectBodyList(BookResponse.class)
.hasSize(0);
}
@Test
void testGetBooksWhenThereIsOne() {
Book book = getDefaultBook();
when(bookService.getBooks()).thenReturn(Flux.just(book));
webTestClient.get()
.uri(API_BOOKS_URL)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_NDJSON_VALUE)
.expectBodyList(BookResponse.class)
.consumeWith(response -> {
assertThat(response.getResponseBody()).isNotNull();
assertThat(response.getResponseBody().get(0).id()).isEqualTo(book.getId());
assertThat(response.getResponseBody().get(0).title()).isEqualTo(book.getTitle());
assertThat(response.getResponseBody().get(0).author()).isEqualTo(book.getAuthor());
assertThat(response.getResponseBody().get(0).year()).isEqualTo(book.getYear());
});
}
@Test
void testGetBookByImdbIdWhenNonExistent() {
when(bookService.validateAndGetBookById(anyString())).thenReturn(Mono.error(new BookNotFoundException("123")));
webTestClient.get()
.uri(API_BOOKS_ID_URL.formatted("123"))
.exchange()
.expectStatus().isNotFound()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(BookResponse.class);
}
@Test
void testGetBookByImdbIdWhenExistent() {
Book book = getDefaultBook();
when(bookService.validateAndGetBookById(anyString())).thenReturn(Mono.just(book));
webTestClient.get()
.uri(API_BOOKS_ID_URL.formatted("123"))
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(BookResponse.class)
.consumeWith(response -> {
assertThat(response.getResponseBody()).isNotNull();
assertThat(response.getResponseBody().id()).isEqualTo(book.getId());
assertThat(response.getResponseBody().title()).isEqualTo(book.getTitle());
assertThat(response.getResponseBody().author()).isEqualTo(book.getAuthor());
assertThat(response.getResponseBody().year()).isEqualTo(book.getYear());
});
}
@Test
void testCreateBook() {
Book book = getDefaultBook();
when(bookService.saveBook(any(Book.class))).thenReturn(Mono.just(book));
CreateBookRequest createBookRequest = new CreateBookRequest("title", "author", 2023);
webTestClient.post()
.uri(API_BOOKS_URL)
.body(Mono.just(createBookRequest), CreateBookRequest.class)
.exchange()
.expectStatus().isCreated()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(BookResponse.class)
.consumeWith(response -> {
assertThat(response.getResponseBody()).isNotNull();
assertThat(response.getResponseBody().id()).isEqualTo(book.getId());
assertThat(response.getResponseBody().title()).isEqualTo(book.getTitle());
assertThat(response.getResponseBody().author()).isEqualTo(book.getAuthor());
assertThat(response.getResponseBody().year()).isEqualTo(book.getYear());
});
}
@Test
void testUpdateBook() {
Book book = getDefaultBook();
UpdateBookRequest updateBookRequest = new UpdateBookRequest("newTitle", "newActors", 2024);
when(bookService.validateAndGetBookById(anyString())).thenReturn(Mono.just(book));
when(bookService.saveBook(any(Book.class))).thenReturn(Mono.just(book));
webTestClient.patch()
.uri(API_BOOKS_ID_URL.formatted("123"))
.body(Mono.just(updateBookRequest), UpdateBookRequest.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(BookResponse.class)
.consumeWith(response -> {
assertThat(response.getResponseBody()).isNotNull();
assertThat(response.getResponseBody().id()).isEqualTo(book.getId());
assertThat(response.getResponseBody().title()).isEqualTo(updateBookRequest.title());
assertThat(response.getResponseBody().author()).isEqualTo(updateBookRequest.author());
assertThat(response.getResponseBody().year()).isEqualTo(updateBookRequest.year());
});
}
@Test
void testDeleteBookWhenExistent() {
Book book = getDefaultBook();
when(bookService.validateAndGetBookById(anyString())).thenReturn(Mono.just(book));
when(bookService.deleteBook(any(Book.class))).thenReturn(Mono.empty());
webTestClient.delete()
.uri(API_BOOKS_ID_URL.formatted("123"))
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(BookResponse.class);
}
@Test
void testDeleteBookWhenNonExistent() {
when(bookService.validateAndGetBookById(anyString())).thenReturn(Mono.error(new BookNotFoundException("123")));
webTestClient.delete()
.uri(API_BOOKS_ID_URL.formatted("123"))
.exchange()
.expectStatus().isNotFound()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(BookResponse.class);
}
private Book getDefaultBook() {
return new Book("123", "title", "author", 2023);
}
private static final String API_BOOKS_URL = "/api/books";
private static final String API_BOOKS_ID_URL = "/api/books/%s";
}This BookControllerTest is designed to test the functionality of the BookController class, which handles HTTP requests related to books in a reactive Spring WebFlux application. Let's break down the key components of this test class:
Annotations
@WebFluxTest(BookController.class): This annotation configures the test to focus only on the components relevant to theBookController. It sets up a minimal Spring application context for testing the web layer;@Import(BookMapperImpl.class): Imports theBookMapperImplclass, indicating that the test will focus on the functionality provided by this specific mapper implementation.
Fields
@Autowired WebTestClient webTestClient: Injects aWebTestClientinstance, which is a tool for testing reactive web applications. It allows making HTTP requests and asserting the responses;@MockBean BookService bookService: Injects a mock bean for theBookService. The actual implementation is replaced with a mock, allowing control over the service's behavior during testing.
Test Methods
testGetBooksWhenThereIsNone(): Tests theGETendpoint for retrieving books when there are none. It expects an empty response and verifies the HTTP status, content type, and body size;testGetBooksWhenThereIsOne(): Tests theGETendpoint for retrieving books when there is one. It expects a response containing the book's details and verifies the HTTP status, content type, and body content;testGetBookByImdbIdWhenNonExistent(): Tests theGETendpoint for retrieving a book by its ID when the book does not exist. It expects a404 Not Foundresponse;testGetBookByImdbIdWhenExistent(): Tests theGETendpoint for retrieving a book by its ID when the book exists. It expects a response containing the book's details;testCreateBook(): Tests thePOSTendpoint for creating a new book. It expects a201 Createdresponse and verifies the created book's details in the response body;testUpdateBook(): Tests thePATCHendpoint for updating an existing book. It expects an200 OKresponse and verifies the updated book's details in the response body;testDeleteBookWhenExistent(): Tests theDELETEendpoint for deleting an existing book. It expects a200 OKresponse and verifies the deleted book's details in the response body;testDeleteBookWhenNonExistent(): Tests theDELETEendpoint for deleting a non-existing book. It expects a404 Not Foundresponse.
Running Unit Tests
In a terminal and inside the book-api root folder, run the following command to start the tests:
./mvnw clean testAt the end, all the tests should pass.
Conclusion
In this article, we have implemented unit tests for a Spring Boot Reactive application called Book API. We have added tests for the mapper, service, controller and DTOs. At the end, we ran the test cases to certify that they are all green and passing.
Support and Engagement
If you enjoyed this article and would like to show your support, please consider taking the following actions:
- 👏 Engage by clapping, highlighting, and replying to my story. I’ll be happy to answer any of your questions;
- 🌐 Share my story on Social Media;
- 🔔 Follow me on: Medium | LinkedIn | Twitter;
- ✉️ Subscribe to my newsletter, so you don’t miss out on my latest posts.




