
지금까지 로그인 기능은 브라우저에서 직접 확인했다.
기존 브라우저 구동 테스트 흐름
앱 실행 ➡️ 아이디와 비밀번호 입력 ➡️ 로그인 버튼 클릭 ➡️ 메시지 창이 뜨는지 확인
이 방법은 화면에서 어떻게 보이는지 확인하기에는 좋았다.
하지만 LoginService 안의 로직을 수정할 때마다 정상 로그인이 되는 아이디나 틀린 비밀번호 같은 경우 다시 입력해야 한다. 금번 경우에 대해 일일이 로그인 규칙이 틀어지면 사람이 매번 같은 과정을 빠짐없이 반복하기가 어려울 것 같았다.
그래서 이번에는 로그인 화면이 아니라 로그인 서비스 자체가 자동으로 검증하는 테스트 프로젝트를 만들어 보기로 했다.
이번 작업의 목표
이번에는 처음엔 테스트를 많이 만들지는 않겠다.
먼저 아래 흐름이 실제 동작하는지 확인한다.
첫 단위 테스트 실행 흐름
테스트 프로젝트 생성 ➡️ 앱 프로젝트 참조 ➡️ LoginService 직접 생성 ➡️ 정상 로그인 요청 전달 ➡️ LoginResult 검증
테스트의 대상은 화면 전체가 아니라 LoginService 하나다.
Home.razor
👉 이번 단위 테스트의 직접 대상이 아님
LoginService
👉 이번 단위 테스트의 검증 대상
xUnit 테스트 프로젝트 만들기
솔루션 폴더에 테스트 전용 프로젝트를 추가했다.
dotnet new xunit -n Study.Web.Tests -f net10.0
구조는 대략 다음과 같이 나뉜다.
Study.Web
👉 실제 Blazor 애플리케이션
Study.Web.Tests
👉 실제 코드를 검증하는 테스트 프로젝트
테스트 코드를 실제 프로그램과 분리해 두는 것이 이해하기가 편해서 관리하기 쉬웠다.
앱 실행 코드와 테스트 코드의 분리
실행 코드(사용자에게 제공하는 기능) ➡️ 테스트 코드(실행 코드 동작이 예상대로 동작하는지 확인)
솔루션에 테스트 프로젝트 추가하기
프로젝트 폴더를 만들었다고 해서 자동으로 솔루션에 포함되는 것은 아니다.
다음 명령으로 테스트 프로젝트를 솔루션에 추가했다.
dotnet sln add .\Study.Web.Tests\Study.Web.Tests.csproj
이제 솔루션에 테스트 프로젝트가 들어왔다.
Study.Web
Study.Web.Tests
루트에서 dotnet build를 실행하면 두 프로젝트가 함께 빌드된다.
테스트 프로젝트에서 앱 프로젝트 참조하기
테스트하려는 LoginService, LoginRequest, LoginResult는 Study.Web 프로젝트에 있다.
그래서 테스트 프로젝트가 실제 프로젝트를 참조하도록 연결했다.
dotnet add .\Study.Web.Tests\Study.Web.Tests.csproj reference .\Study.Web\Study.Web.csproj
의존 방향은 다음과 같다.
Study.Web.Tests
⬇️ 참조
Study.Web
반대 방향으로 참조하면 안 된다.
Study.Web
❌
Study.Web.Tests
이런 구조는 만들 수는 있지만
실제 애플리케이션이 테스트 프로젝트에 의존하면 실행 코드와 테스트 코드가 꼬여 복잡해지기 때문이다.
첫 테스트의 시나리오 정의하기
첫 번째 테스트는 가장 단순한 정상 로그인으로 정했다.
올바른 아이디와 비밀번호를 입력하면
LoginService가 성공 결과를 반환해야 한다.
테스트 메서드 이름은 조건과 기대 결과를 보이도록 작성했다.
LoginAsync_WhenCredentialsAreValid_ReturnsSuccess
세 부분으로 나눠보면 다음과 같다.
LoginAsync
👉 테스트할 메서드
WhenCredentialsAreValid
👉 어떤 조건에서
ReturnsSuccess
👉 어떤 결과를 기대하는지
문장처럼 읽으면 다음과 같다.
LoginAsync는 로그인 정보가 올바르면 성공을 반환한다.
FakeUserRepository를 사용하는 이유
LoginService는 IUserRepository를 생성자로 받는다.
public LoginService(IUserRepository userRepository)
{
this.userRepository = userRepository;
}
테스트에서도 실제 TestUserRepository를 사용할 수도 있었다.
하지만 그렇게 되면 테스트 실패 시에 어디가 문제인지 함께 검증해야 한다.
테스트 실패
👇
LoginService 문제인가?
TestUserRepository 문제인가?
테스트 데이터가 바뀐 것인가?
이번 목적은 LoginService의 규칙을 확인하는 것이다.
그래서 테스트 파일 안에 아주 단순한 Fake Repository를 만들었다.
private class FakeUserRepository : IUserRepository
{
public Task<UserAccount?> FindByUserIdAsync(string userId)
{
UserAccount? userAccount;
userAccount = null;
if (userId == "sample-admin")
{
userAccount = new UserAccount();
userAccount.UserId = "sample-admin";
userAccount.Password = "1111";
userAccount.UserName = "테스트 관리자";
userAccount.UserGrade = "관리자";
}
return Task.FromResult(userAccount);
}
}
이 Fake Repository의 규칙은 단순하다.

Fake Repository 규칙
sample-admin 이면 ➡️ 사용자 반환, 이외의 아이디면 ➡️ null 반환
실제 DB도 없고, 테스트용 사용자 목록 전체가 필요하지 않다.
LoginService 테스트에 필요한 상황만 제공한다.
첫 정상 로그인 테스트 작성하기
테스트 파일을 다음처럼 작성했다.
using Study.Web.Models;
using Study.Web.Repositories;
using Study.Web.Services;
using Xunit;
namespace Study.Web.Tests.Services
{
public class LoginServiceTests
{
[Fact]
public async Task LoginAsync_WhenCredentialsAreValid_ReturnsSuccess()
{
IUserRepository userRepository;
LoginService loginService;
LoginRequest loginRequest;
LoginResult loginResult;
userRepository = new FakeUserRepository();
loginService = new LoginService(userRepository);
loginRequest = new LoginRequest();
loginRequest.UserId = "sample-admin";
loginRequest.Password = "1111";
loginResult = await loginService.LoginAsync(loginRequest);
Assert.True(loginResult.IsSuccess);
Assert.Equal(
"sample-admin",
loginResult.UserInfo.UserId);
Assert.Equal(
"테스트 관리자",
loginResult.UserInfo.UserName);
Assert.Equal(
"관리자",
loginResult.UserInfo.UserGrade);
}
private class FakeUserRepository : IUserRepository
{
public Task<UserAccount?> FindByUserIdAsync(string userId)
{
UserAccount? userAccount;
userAccount = null;
if (userId == "sample-admin")
{
userAccount = new UserAccount();
userAccount.UserId = "sample-admin";
userAccount.Password = "1111";
userAccount.UserName = "테스트 관리자";
userAccount.UserGrade = "관리자";
}
return Task.FromResult(userAccount);
}
}
}
}
블로그에 사용하는 계정과 비밀번호는 실제 정보가 아닌 임시 값으로 바꿨다.
실제 준비에서는 비밀번호를 코드가 평문으로 저장하지 않아야 한다.
[Fact]란 무엇인가
테스트 메서드 위에는 [Fact]가 붙는다.
[Fact]
public async Task LoginAsync_WhenCredentialsAreValid_ReturnsSuccess()
[Fact]는 xUnit에게 이 메서드가 실행할 테스트라는 사실을 알려준다.
일반 메서드
👉 xUnit이 테스트로 실행하지 않음
[Fact] 붙은 메서드
👉 xUnit이 테스트로 발견하고 실행
Java의 JUnit에서 사용하는 @Test와 비슷하게 느껴졌다.
@Test
void loginWithValidCredentialsReturnsSuccess()
{
}
Arrange, Act, Assert 나누어보기
단위 테스트는 보통 세 단계를 거친다.
Arrange: 준비
userRepository = new FakeUserRepository();
loginService = new LoginService(userRepository);
loginRequest = new LoginRequest();
loginRequest.UserId = "sample-admin";
loginRequest.Password = "1111";
테스트에 필요한 객체와 입력값을 준비한다.
Act: 실행
loginResult = await loginService.LoginAsync(loginRequest);
실제로 확인하려는 메서드를 호출한다.
Assert: 검증
Assert.True(loginResult.IsSuccess);
Assert.Equal(
"sample-admin",
loginResult.UserInfo.UserId);
예상하는 결과와 실제 결과가 같은지 확인한다.
전체 흐름은 다음과 같다.
단위 Arrange, Act, Assert 흐름
Arrange (준비) ➡️ Act (실행) ➡️ Assert (검증)
테스트 코드를 이렇게 단계를 나누어보니 무엇을 준비하고 무엇을 실행하며, 무엇을 확인하는지 구분하기 쉬웠다.
Assert는 무엇을 확인하는가?
정상 로그인 테스트에서는 성공 여부만 확인하지 않았다.
Assert.True(loginResult.IsSuccess);
사용자 정보까지 올바르게 전달됐는지 확인했다.
Assert.Equal(
"sample-admin",
loginResult.UserInfo.UserId);
Assert.Equal(
"테스트 관리자",
loginResult.UserInfo.UserName);
Assert.Equal(
"관리자",
loginResult.UserInfo.UserGrade);
로그인 성공만 확인하면 UserInfo가 비어 있어도 테스트를 통과해버릴 수 있다.
그래서 이번 테스트에서는 다음 규칙을 함께 고정했다.
정상 계정이면 로그인에 성공한다.
로그인한 사용자의 아이디가 결과에 들어간다.
사용자 이름과 등급도 결과에 들어간다.
dotnet test 실행하기
테스트는 다음 명령으로 실행했다.
dotnet test
정상적으로 실행되면 대략 다음 순서로 진행된다.
필요한 패키지 복원
👇
Study.Web 빌드
👇
Study.Web.Tests 빌드
👇
xUnit 테스트 실행
👇
테스트 결과 집계
내 테스트를 통과했을 때의 결과 화면은 다음과 비슷하다.
테스트 통계: 1
성공: 1
실패: 0
건너뜀: 0
xUnit과 dotnet test는 같은 것일까
처음에는 둘이 같은 것으로 생각했다.
하지만 역할이 나뉘어 있었다.
xUnit
👉 테스트 작성 규칙 제공
👉 [Fact], [Theory], Assert 제공
xUnit VSTest Adapter
👉 xUnit 테스트를 실행 플랫폼이 찾도록 연결
VSTest
👉 발견된 테스트를 실행
dotnet test
👉 복원, 빌드, 테스트 발견과 실행 과정을 시작
즉 dotnet test가 전체 실행을 시작하고, xUnit은 테스트를 표현하고 검증하는 도구를 제공한다.
테스트가 정말 동작하는지 일부러 실패시켜보기
테스트가 항상 통과하기만 하면 제대로 검증하는지 알기 어렵다.
그래서 예상값을 일부러 틀리게 바꿔봤다.
Assert.Equal(
"잘못된 이름",
loginResult.UserInfo.UserName);
다시 dotnet test를 실행하니 예상값과 실제값이 다르다는 실패 결과가 나왔다.
Expected: 잘못된 이름
Actual: 테스트 관리자
확인 후 다시 원래 값으로 돌렸다.
Assert.Equal(
"테스트 관리자",
loginResult.UserInfo.UserName);
이 실험을 통해 테스트가 단순히 실행하는 것이 아니라 실제 결과를 비교하고 있다는 것을 확인할 수 있었다.
이번 작업 결과
브라우저 없이 코드만으로 로그인 서비스의 정상 경로를 확인할 수 있게 되었다.
FakeUserRepository 준비
👇
LoginService 생성
👇
정상 LoginRequest 전달
👇
LoginResult 검증
또한 IUserRepository를 생성자로 주입받게 도입한 이유도 조금 더 분명해졌다.
실제 실행 시
👉 TestUserRepository 전달
단위 테스트 시
👉 FakeUserRepository 전달
나중 실제 DB 연동 시
👉 DatabaseUserRepository 전달 가능
같은 LoginService라도 필요한 구현체를 상황에 따라 바꿔 끼울 수 있었다.
오늘 배운 점
- xUnit은 .NET에서 단위 테스트를 작성하는 프레임워크다.
- 테스트 프로젝트를 실제 애플리케이션과 분리해서 관리할 수 있다.
- 테스트 프로젝트가 실제 프로젝트를 참조해야 준비된 모델을 사용할 수 있다.
[Fact]는 xUnit이 실행할 테스트 메서드를 표시한다.Assert.True()는 값이 참인지 확인한다.Assert.Equal()은 예상값과 실제값을 비교한다.- 단위 테스트는 Arrange, Act, Assert 순서를 따른다.
- Fake Repository를 사용하면 외부 의존성을 최소화하고 분리해서 서비스의 규칙만 확인할 수 있다.
dotnet test는 복원, 빌드, 테스트 발견과 실행 과정을 한 번에 처리한다.- 일부러 실패시켜보면 테스트가 실제로 무엇을 검증하는지 확인할 수 있다.
'language > blazor' 카테고리의 다른 글
| [Blazor Web App] 13. 로그인 흐름을 비동기로 바꾸기 - Task, async, await 이해하기 (0) | 2026.08.20 |
|---|---|
| [Blazor Web App] 12. Repository에 인터페이스 적용하기 - 생성자 주입과 DI 이해하기 (0) | 2026.08.16 |
| [Blazor Web App] 11. 로그인 데이터 분리하기 - UserAccount와 TestUserRepository 만들기 (0) | 2026.08.08 |
| [Blazor Web App] 10. UserInfo 생성 책임을 LoginService로 옮기기 (0) | 2026.08.07 |
| [Blazor Web App] 09. CSS 구조 정리하기 - app.css와 .razor.css 역할 나누기 (0) | 2026.07.31 |