How to Write Test Cases for Login Flow (With Examples)
How to Write Test Cases for Login Flow (With Examples) is a practical guide that shows you how to design, organize, and execute test cases that give real confidence in one of the most critical user pa
How to Write Test Cases for Login Flow (With Examples) is a practical guide that shows you how to design, organize, and execute test cases that give real confidence in one of the most critical user pathways in any application. Login is often the first interaction a user has with a system, and defects here can block access, damage trust, or expose security gaps. This article walks you through the full lifecycle of test‑case creation: from gathering requirements to structuring each case, from building a comprehensive matrix to linking cases to requirements, prioritizing effort, and blending manual design with automated execution and autonomous exploration. You will find concrete examples, a ready‑to‑use test table with over twenty cases, data‑setup strategies, checklists, and code snippets that you can adapt to Android, iOS, or web stacks. By the end, you will have a repeatable process you can bookmark and apply to any login flow, whether you are writing tests by hand or letting a tool like SUSATest generate regression scripts from its autonomous runs.
1. Understanding Login Flow Requirements
Before you write a single test case, you need a clear picture of what the login flow is supposed to do. Requirements may live in user stories, acceptance criteria, API contracts, or design sketches. Treat them as the source of truth for every test you will later create.
1.1 Extracting User Stories and Acceptance Criteria
Start by locating the story that describes login. A user can authenticate with email/password. Typical acceptance criteria might read:
- Given a registered user, when they enter correct credentials and press Sign In, then they are redirected to the dashboard and a session token is stored.
- Given an unregistered email, when they attempt to sign in, then an error message “Invalid email or password” appears.
- Given a locked account, when they attempt to sign in after five failed attempts, then the account is temporarily disabled and a “Too many attempts” message appears.
- Given a user with accessibility needs, when they navigate the login screen with a screen reader, then all fields and buttons have appropriate ARIA labels.
Write each criterion in Given/When/Then form; this makes it trivial to map to test cases later.
1.2 Identifying Non‑Functional Aspects
Login also carries performance, security, and usability expectations. Note down:
- Maximum acceptable response time for credential verification (e.g., < 2 seconds).
- Password policy: minimum length, required character sets, lockout threshold.
- Rate‑limiting: how many failed attempts per IP/account before a CAPTCHA or delay.
- Accessibility: WCAG 2.1 AA contrast, focus order, label association.
- Internationalization: support for Unicode usernames, right‑to‑left layouts.
Capture these as separate requirement items; they will generate additional test categories (boundary, security, accessibility).
1.3 Creating a Requirements Traceability Matrix (RTM)
A lightweight RTM can be a simple spreadsheet with three columns: Requirement ID, Description, Linked Test Case IDs. As you write cases, fill in the Test Case IDs column. This gives you instant visibility: if a requirement has zero linked cases, you know you missed something. If a requirement has many cases, you can review for redundancy.
2. Anatomy of a Test Case
A well‑structured test case is readable by both humans and test‑management tools. Consistency reduces ambiguity and makes maintenance easier.
2.1 Core Fields
| Field | Purpose |
|---|---|
| Test ID | Unique identifier (e.g., LOGIN‑001). Enables traceability and referencing in defect reports. |
| Title | Short, descriptive summary (e.g., “Valid credentials redirect to dashboard”). |
| Preconditions | State that must be true before execution (e.g., “User exists in test database with password ‘Abcdef1!’”). |
| Test Steps | Ordered actions the tester performs. Each step should be atomic and observable. |
| Expected Result | What the system should do after the last step (e.g., “Dashboard page loads, URL contains /dashboard, auth token present in storage”). |
| Postconditions | Optional cleanup (e.g., “Log out user to reset state”). |
| Data | Input values used (e.g., email, password). Can be inline or referenced from a data set. |
| Attachments | Screenshots, mockups, or API contracts that help clarify the step. |
2.2 Writing Effective Steps
Use imperative mood and avoid conjunctions that hide multiple actions. Bad: “Enter email and password then click Sign In and verify you are logged in.” Good:
- Open the login screen.
- In the Email field, enter
user@example.com. - In the Password field, enter
Abcdef1!. - Tap the Sign In button.
- Verify that the dashboard screen appears within 2 seconds.
- Verify that the auth token is stored in secure storage.
Each step maps to a single UI interaction or verification point, which simplifies automation.
2.3 Leveraging Templates
Most test‑management tools (TestRail, Zephyr, Xray) allow you to define a template. Create a login‑specific template that already contains the Preconditions and Postconditions sections filled with common placeholders (e.g., “Ensure the app is installed and cleared of previous session data”). Then you only need to vary Title, Steps, Expected Result, and Data.
2.4 Note on Auto‑Generated Scripts
When you later run an autonomous explorer such as SUSATest, it can observe the steps you defined and output ready‑to‑run Appium (Android) or Playwright (Web) scripts. This bridges the gap between manual case design and automated regression without rewriting logic.
3. Categorizing Test Cases: Positive, Negative, Edge, Boundary
Grouping cases by intent helps you prioritize and ensures you cover complementary aspects of the flow.
3.1 Positive Cases
Positive cases verify that the happy path works under normal conditions. They confirm that valid inputs lead to the expected successful outcome. Examples:
- Correct email and password.
- Valid social‑login provider (Google, Apple) with a test account.
- Remember‑me checkbox persisting session after app restart.
3.2 Negative Cases
Negative cases validate that the system correctly rejects invalid or malicious input. They should also check that error messages are helpful and that the system does not leak information (e.g., distinguishing “unknown email” vs “wrong password”).
- Non‑existent email.
- Correct email, wrong password.
- Password missing required special character.
- Email field contains SQL injection string (
' OR 1=1--). - Submitting the form with both fields empty.
3.3 Edge Cases
Edge cases sit at the borders of valid input domains or involve unusual interaction patterns. They often uncover bugs that only appear under stress or with specific data sets.
- Email with leading/trailing spaces.
- Password consisting only of spaces.
- Very long email (254 characters, the RFC limit).
- Email with multiple consecutive dots (
user..name@domain.com). - Password containing Unicode emojis.
- Rapid double‑tap on Sign In button.
- Switching network from Wi‑Fi to cellular mid‑authentication.
3.4 Boundary Cases
Boundary cases test the limits defined by requirements (length, count, rate). They are a subset of edge cases but deserve explicit mention because they are easy to automate with data‑driven loops.
- Minimum password length (e.g., 8 characters) – one less (7) should fail, exactly 8 should pass.
- Maximum password length (e.g., 64) – 64 passes, 65 fails.
- Username length limits (if applicable).
- Rate‑limit threshold: after N failed attempts, the next attempt shows CAPTCHA or delay.
- Concurrent login attempts from same credentials on two devices.
4. Building a Test Matrix for Login Flow
Below is a concrete matrix of 24 test cases that covers positive, negative, edge, and boundary scenarios. Feel free to copy it into your test‑management tool and adapt the IDs, data, or expected results to your specific implementation.
| TC ID | Title | Preconditions | Steps | Expected Result | Type | Priority |
|---|---|---|---|---|---|---|
| LOGIN-001 | Valid email/password redirects to dashboard | User alice@example.com exists with password Secure12! | 1. Launch app 2. Enter email alice@example.com 3. Enter password Secure12! 4. Tap Sign In | Dashboard loads, URL /dashboard, auth token stored, welcome message shows Alice’s name | Positive | P0 |
| LOGIN-002 | Valid social login (Google) | Test Google account testuser@gmail.com with password known | 1. Launch app 2. Tap Sign In with Google 3. Enter Google credentials 4. Consent | Dashboard loads, token present, user profile shows Google email | Positive | P0 |
| LOGIN-003 | Remember‑me keeps session after restart | User bob@example.com exists, remember‑me enabled | 1. Log in with valid creds, check Remember me 2. Close app 3. Reopen app | App opens directly to dashboard without prompting for credentials | Positive | P1 |
| LOGIN-004 | Invalid email format shows error | No preconditions needed | 1. Launch app 2. Enter email notanemail 3. Enter any password 4. Tap Sign In | Inline error: “Please enter a valid email address” | Negative | P0 |
| LOGIN-005 | Non‑existent email returns generic error | Ensure no user with email unknown@domain.com | 1. Launch app 2. Enter email unknown@domain.com 3. Enter password AnyPass1! 4. Tap Sign In | Toast: “Invalid email or password” (does not reveal whether email exists) | Negative | P0 |
| LOGIN-006 | Correct email, wrong password | User carol@example.com exists with password RealPass1! | 1. Launch app 2. Enter email carol@example.com 3. Enter password WrongPass 4. Tap Sign In | Error: “Invalid email or password” | Negative | P0 |
| LOGIN-007 | Password missing required special char | Policy: at least one special character | 1. Launch app 2. Enter email dave@example.com 3. Enter password NoSpecial123 (only letters/numbers) 4. Tap Sign In | Error: “Password must contain at least one special character” | Negative | P1 |
| LOGIN-008 | Password too short (below min) | Policy: min length 8 | 1. Launch app 2. Enter email eve@example.com 3. Enter password short (5 chars) 4. Tap Sign In | Error: “Password must be at least 8 characters” characters long” | Negative | P1 |
| LOGIN-009 | Password at max length (boundary) | Policy: max length 64 | 1. Launch app 2. Enter email frank@example.com 3. Enter password a repeated 64 times 4. Tap Sign In | Login succeeds, token issued | Boundary | P2 |
| LOGIN-010 | Password exceeds max length (boundary) | Policy: max length 64 | 1. Launch app 2. Enter email grace@example.com 3. Enter password a repeated 65 times 4. Tap Sign In | Error: “Password must be no longer than 64 characters” | Boundary | P2 |
| LOGIN-011 | Email with leading/trailing spaces | No preconditions | 1. Launch app 2. Enter email space@example.com (note spaces) 3. Enter valid password 4. Tap Sign In | System trims spaces and logs in successfully OR shows error if trimming not implemented (specify expected) | Edge | P2 |
| LOGIN-012 | Email with consecutive dots | RFC allows but some systems reject | 1. Launch app 2. Enter email user..name@example.com 3. Enter valid password 4. Tap Sign In | Either success (if allowed) or error “Invalid email format” (based on spec) | Edge | P2 |
| LOGIN-013 | Password containing emoji | Unicode support | 1. Launch app 2. Enter email henry@example.com 3. Enter password Pass😀word! 4. Tap Sign In | Login succeeds (if Unicode allowed) or error if rejected | Edge | P2 |
| LOGIN-014 | SQL injection attempt in email field | No preconditions | 1. Launch app 2. Enter email ' OR 1=1-- 3. Enter any password 4. Tap Sign In | Error: “Invalid email address” (no SQL execution) | Negative (Security) | P0 |
| LOGIN-015 | XSS attempt in password field | No preconditions | 1. Launch app 2. Enter email victim@example.com 3. Enter password 4. Tap Sign In | Error or sanitized input; no script execution in subsequent pages | Negative (Security) | P0 |
| LOGIN-016 | Empty fields submission | No preconditions | 1. Launch app 2. Leave email blank 3. Leave password blank 4. Tap Sign In | Inline errors for both fields: “Email is required”, “Password is required” | Negative | P0 |
| LOGIN-017 | Whitespace only email | No preconditions | 1. Launch app 2. Enter email (spaces) 3. Enter valid password 4. Tap Sign In | Error: “Email is required” or “Invalid email address” | Edge | P1 |
| LOGIN-018 | Whitespace only password | No preconditions | 1. Launch app 2. Enter valid email 3. Enter password (spaces) 4. Tap Sign In | Error: “Password is required” or “Password must contain at least …” | Edge | P1 |
| LOGIN-019 | Rapid double‑tap on Sign In | No preconditions | 1. Launch app 2. Enter valid creds 3. Tap Sign In twice within 200ms | Only one authentication request sent; second tap ignored or shows “Already processing” | Edge | P2 |
| LOGIN-020 | Network switch mid‑authentication | Device connected to Wi‑Fi | 1. Launch app 2. Enter valid creds 3. Tap Sign In 4. Immediately disable Wi‑Fi and enable cellular | Authentication either completes successfully with retry or shows clear network error; no crash | Edge | P2 |
| LOGIN-021 | Account lockout after 5 failed attempts | Lockout threshold = 5 | 1. Launch app 2. Enter valid email lock@example.com 3. Enter wrong password 4. Tap Sign In (repeat 4 more times) 5. On 6th attempt, enter correct password | After 5th failed attempt: message “Account temporarily locked”. 6th attempt (even with correct creds) shows same lockout message. | Negative | P0 |
| LOGIN-022 | CAPTCHA appears after rate‑limit threshold | Rate‑limit = 10 failures/min per IP | 1. Using a script or manual rapid attempts, fail login 10 times quickly 2. On 11th attempt, enter correct credentials | CAPTCHA widget appears; login blocked until solved | Negative | P1 |
| LOGIN-023 | Accessibility: label association | Screen reader (TalkBack/VoiceOver) enabled | 1. Launch app 2. Focus on Email field 3. Activate screen reader | Reader announces “Email, edit text, required”. Same for Password and Sign In button | Accessibility | P1 |
| LOGIN-024 | Performance: login under 2 seconds | Network latency simulated at 150ms RTT | 1. Launch app 2. Enter valid creds 3. Tap Sign In 4. Measure time to dashboard load | Time to dashboard ≤ 2000ms | Performance | P1 |
How to read the table
- Type tells you the logical bucket (Positive, Negative, Edge, Boundary, Security, Accessibility, Performance).
- Priority follows a simple P0‑P3 scale where P0 = must‑run for every build, P1 = important for each release, P2 = run nightly or weekly, P3 = exploratory.
- Preconditions are kept minimal; you can satisfy them with a test data setup script (see Section 5).
- Expected Result includes both functional (UI navigation) and non‑functional checks (token storage, timing, error messages).
You can import this CSV into most test‑case managers and then add automation IDs or tags.
5. Data Setup and Test Data Management
Reliable login tests depend on predictable data. Flaky data leads to false positives and erodes confidence.
5.1 Static vs Dynamic Data
- Static data: A fixed set of users stored in a version‑controlled seed file (e.g.,
login_seed.json). Useful for UI tests that run against a stable test environment. - Dynamic data: Generated on‑the‑fly by a fixture factory or API call (e.g., POST
/test-usersreturns a freshly created account). Ideal for parallel runs and for testing account‑creation flows.
Choose static for smoke suites and dynamic for regression or load‑testing suites.
5.2 Example: JSON Seed File
[
{
"email": "alice@example.com",
"password": "Secure12!",
"firstName": "Alice",
"lastName": "Anderson",
"enabled": true
},
{
"email": "bob@example.com",
"password": "Another!456",
"firstName": "Bob",
"lastName": "Baker",
"enabled": false
}
]
Load this file in your test setup script (see code snippets in Section 7) and insert the records into the test database or call a test‑only endpoint that creates them.
5.3 Using Factory Libraries
In Java with DataFaker or Java‑Faker:
public User randomUser() {
return new User()
.setEmail(faker.internet().emailAddress())
.setPassword(faker.internet().password(10, 20, true, true, true))
.setEnabled(true);
}
In Python with factory_boy:
class UserFactory(factory.Factory):
class Meta:
model = User
email = factory.Faker('email')
password = factory.LazyAttribute(lambda _: f"{factory.Faker('password').generate(length=12)}Aa!")
is_active = True
Call the factory in a beforeEach hook to ensure each test starts with a clean slate.
5.4 Mocking External Identity Providers
If your login delegates to OAuth providers, you can avoid flaky third‑party calls by mocking the token endpoint. Tools like WireMock (Java) or msw (Mock Service Worker, JavaScript) let you define:
- A successful token response with a known
access_token. - An error response (
invalid_grant) to simulate bad credentials. - A delayed response to test timeout handling.
Record the mock mappings in source control so they travel with your test code.
5.5 Cleanup Strategies
After each test, you should:
- Delete the test user if you created it dynamically.
- Clear app data or web storage (localStorage, sessionStorage, cookies) to remove leftover tokens.
- Reset any rate‑limit counters in the mock server.
Automate cleanup in an afterEach hook; otherwise, tests may start interfering with each other.
6. Prioritization and Risk‑Based Testing
Not all test cases carry equal weight. Use a risk matrix to decide what to run first, especially when time is limited.
6.1 Building a Simple Risk Matrix
| Impact \ Likelihood | Low (1) | Medium (2) | High (3) |
|---|---|---|---|
| Low (1) | 1 | 2 | 3 |
| Medium (2) | 2 | 4 | 6 |
| High (3) | 3 | 6 | 9 |
Score each test case by assigning Impact (1‑3) based on business consequence (e.g., lockout = high, UI typo = low) and Likelihood (1‑3) based on historical defect density or complexity. Multiply to get a Risk Priority Number (RPN). Higher RPN → higher execution priority.
6.2 Example RPN Assignment
| TC ID | Impact | Likelihood | RPN | Priority (derived) |
|---|---|---|---|---|
| LOGIN-001 | 3 | 3 | 9 | P0 |
| LOGIN-004 | 2 | 3 | 6 | P0 |
| LOGIN-005 | 2 | 3 | 6 | P0 |
| LOGIN-006 | 2 | 3 | 6 | P0 |
| LOGIN-007 | 2 | 2 | 4 | P1 |
| LOGIN-008 | 2 | 2 | 4 | P1 |
| LOGIN-009 | 1 | 2 | 2 | P2 |
| LOGIN-010 | 1 | 2 | 2 | P2 |
| LOGIN-011 | 1 | 1 | 1 | P3 |
| LOGIN-012 | 1 | 1 | 1 | P3 |
| LOGIN-013 | 1 | 1 | 1 | P3 |
| LOGIN-014 | 3 | 2 | 6 | P0 (security) |
| LOGIN-015 | 3 | 2 | 6 | P0 (security) |
| LOGIN-016 | 2 | 3 | 6 | P0 |
| LOGIN-017 | 1 | 2 | 2 | P2 |
| LOGIN-018 | 1 | 2 | 2 | P2 |
| LOGIN-019 | 1 | 2 | 2 | P2 |
| LOGIN-020 | 2 | 2 | 4 | P1 |
| LOGIN-021 | 3 | 3 | 9 | P0 |
| LOGIN-022 | 2 | 2 | 4 | P1 |
| LOGIN-023 | 2 | 2 | 4 | P1 (accessibility) |
| LOGIN-024 | 2 | 3 | 6 | P1 (performance) |
Map RPN ranges to your P0‑P3 buckets (e.g., 7‑9 → P0, 4‑6 → P1, 2‑3 → P2, 1 → P3). This gives you a defensible ordering that stakeholders can review.
6.3 Traceability to Requirements
Link each TC ID to the requirement IDs from your RTM. For instance:
- LOGIN-001 maps to REQ‑LGN‑01 (valid credentials → dashboard).
- LOGIN-004 maps to REQ‑LGN‑03 (invalid email format → error).
- LOGIN-014 maps to REQ‑SEC‑02 (input sanitization prevents injection).
When a requirement changes, you can instantly see which test cases need review by filtering the RTM.
7. Manual vs Automated Approaches
Designing test cases manually is the first step; turning them into automated checks provides regression safety and enables frequent execution.
7.1 Writing Manual Test Cases
Use the template from Section 2.2. Keep the language imperative and avoid vague phrases like “verify that the user is logged in”. Instead, specify the observable outcome: “Check that the dashboard header contains the user’s first name” or “Confirm that an auth token is present in SecureStorage”.
When you write the case, also note any test data needed (email, password) and any setup (e.g., “Ensure the user is not already logged in”). This makes hand‑off to automation straightforward.
7.2 Converting to Automated Scripts
Below are minimal but complete examples for Android (Appium with Java) and web (Playwright with TypeScript). Adjust selectors, timeouts, and data‑loading to match your project.
#### 7.2.1 Appium Android Example (Java)
import io.appium.java_client.AppiumDriver;
import io.appium.java_client.MobileElement;
import io.appium.java_client.android.AndroidDriver;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.remote.DesiredCapabilities;
import java.net.URL;
import java.util.concurrent.TimeUnit;
public class LoginFlowTest {
private AppiumDriver<MobileElement> driver;
@Before
public void setUp() throws Exception {
DesiredCapabilities caps = new DesiredCapabilities();
caps.setCapability("platformName", "Android");
caps.setCapability("deviceName", "Pixel_4_API_33");
caps.setCapability("appPackage", "com.example.myapp");
caps.setCapability("appActivity", ".ui.login.LoginActivity");
caps.setCapability("automationName", "UiAutomator2");
driver = new AndroidDriver<>(new URL("http://127.0.0.1:4723/wd/hub"), caps);
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
}
@After
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
@Test
public void testValidLogin() {
// Precondition: ensure test user exists (could be done via API before suite)
driver.findElement(By.id("email_input")).sendKeys("alice@example.com");
driver.findElement(By.id("password_input")).sendKeys("Secure12!");
driver.findElement(By.id("sign_in_button")).click();
// Expected: dashboard appears within 5 seconds
MobileElement dashboard = driver.waitForElement(By.id("dashboard_header"), 5);
assert dashboard.isDisplayed();
assert dashboard.getText().equals("Welcome, Alice");
}
}
Notes
- Replace the
idlocators with the actual resource IDs from your app. - The
waitForElementhelper is a small utility that polls until the element appears or timeout. - For data‑driven runs, replace the hard‑coded strings with values read from a CSV or JSON file using a library like OpenCSV or Jackson.
#### 7.2.2 Playwright Web Example (TypeScript)
import { test, expect } from '@playwright/test';
test.describe('Login Flow', () => {
test('valid credentials redirect to dashboard', async ({ page }) => {
// Precondition: ensure test user exists via API fixture (omitted for brevity)
await page.goto('https://app.example.com/login');
await page.fill('#email', 'alice@example.com');
await page.fill('#password', 'Secure12!');
await page.click('button:has-text("Sign In")');
// Expect navigation to dashboard
await expect(page).toHaveURL(/.*\/dashboard/, { timeout: 5000 });
const welcome = page.locator('#welcome-message');
await expect(welcome).toBeVisible();
await expect(welcome).toHaveText(/Welcome, Alice/i);
});
test('invalid email shows error', async ({ page }) => {
await page.goto('https://app.example.com/login');
await page.fill('#email', 'notanemail');
await page.fill('#password', 'anything');
await page.click('button:has-text("Sign In")');
const error = page.locator('.error-message');
await expect(error).toBeVisible();
await expect(error).toHaveText(/Please enter a valid email address/);
});
});
Notes
- Use
test.use({ storageState: ... })if you need to preserve login state across tests. - For negative cases that involve API mocking, integrate MSW (Mock Service Worker) to intercept the
/authendpoint and return a 401 payload.
7.3 Data‑Driven Execution
Both frameworks support iterating over a data set.
- Appium/JUnit: Use
@ParameterizedTestwith@CsvSourceor read from a JSON file in a@BeforeAllmethod. - Playwright: Use
test.describe.configure({ mode: 'serial' })and a simpleforloop over an array of objects, callingtest.each(Playwright v1.32+).
Example Playwright data‑driven block:
const loginCases = [
{ email: 'alice@example.com', password: 'Secure12!', shouldPass: true },
{ email: 'bob@example.com', password: 'wrong', shouldPass: false },
// … add more from your matrix
];
test.describe('Data‑driven login', () => {
for (const { email, password, shouldPass } of loginCases) {
test(`login with ${email} – ${shouldPass ? 'pass' : 'fail'}`, async ({ page }) => {
await page.goto('https://app.example.com/login');
await page.fill('#email', email);
await page.fill('#password', password);
await page.click('button:has-text("Sign In")');
if (shouldPass) {
await expect(page).toHaveURL(/.*\/dashboard/);
} else {
const error = page.locator('.error-message');
await expect(error).toBeVisible();
}
});
}
});
7.4 Keeping Manual and Automated Cases in Sync
- Store the master test case in a spreadsheet or test‑management tool.
- Export the CSV/JSON to generate the automated test data (scripts can read the same file).
- When a manual case changes, update the source file and re‑run the export; the automated tests stay aligned.
- Tag automated tests with the same TC ID (e.g.,
@TestCaseId("LOGIN-007")) so you can trace failures back to the specification.
8. Leveraging Autonomous Exploration with SUSA
While well‑crafted manual and automated tests
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free