<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aleksandr Singer</title>
    <description>The latest articles on DEV Community by Aleksandr Singer (@aleksandr_singer_e51bc835).</description>
    <link>https://dev.to/aleksandr_singer_e51bc835</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024603%2Fb79ed132-82c6-458b-a2aa-cbe247d7c39d.png</url>
      <title>DEV Community: Aleksandr Singer</title>
      <link>https://dev.to/aleksandr_singer_e51bc835</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aleksandr_singer_e51bc835"/>
    <language>en</language>
    <item>
      <title>Breaking Into Software QA</title>
      <dc:creator>Aleksandr Singer</dc:creator>
      <pubDate>Fri, 10 Jul 2026 23:46:04 +0000</pubDate>
      <link>https://dev.to/aleksandr_singer_e51bc835/breaking-into-software-qa-4cmp</link>
      <guid>https://dev.to/aleksandr_singer_e51bc835/breaking-into-software-qa-4cmp</guid>
      <description>&lt;h1&gt;
  
  
  Testing a Login Feature the Way You'd Actually Do It on the Job
&lt;/h1&gt;

&lt;p&gt;A lot of QA content teaches testing techniques in isolation — here's equivalence partitioning, here's boundary value analysis, here's how to write a bug report. All useful, but it rarely shows how those pieces come together on a single, real feature.&lt;/p&gt;

&lt;p&gt;So here's one walkthrough, start to finish: testing a login feature manually, at the API level, and with automation — the way you'd actually approach it with a sprint deadline, not in a vacuum.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature
&lt;/h2&gt;

&lt;p&gt;A web and mobile app lets a user log in with an email and password. On success, they land on a dashboard. After 5 failed attempts within 10 minutes, the account locks for 15 minutes.&lt;/p&gt;

&lt;p&gt;Simple on the surface. There's more here than "type password, click button."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Manual test design
&lt;/h2&gt;

&lt;p&gt;Before touching a keyboard, map out what actually needs checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Happy path&lt;/strong&gt;: valid email + valid password logs in successfully&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boundary&lt;/strong&gt;: exactly 5 failed attempts triggers lockout; 4 attempts does not&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative&lt;/strong&gt;: wrong password, non-existent email, empty fields, SQL-injection-style input in the email field&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session&lt;/strong&gt;: does refreshing the page keep the user logged in? Does logging out on web also affect the mobile session?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile-specific&lt;/strong&gt;: biometric login after the first successful password login, app backgrounded mid-login&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That boundary case — exactly 5 attempts vs. 4 — is the kind of thing that's easy to skip if you're only thinking in terms of "test the lockout feature" rather than testing the &lt;em&gt;number&lt;/em&gt; itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: API-level tests
&lt;/h2&gt;

&lt;p&gt;UI tests are slow and brittle compared to hitting the API directly. Before automating anything visual, these are worth locking down at the API layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /login&lt;/code&gt; with valid credentials returns &lt;code&gt;200&lt;/code&gt; and a token&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /login&lt;/code&gt; with invalid credentials returns &lt;code&gt;401&lt;/code&gt;, not &lt;code&gt;500&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /login&lt;/code&gt; six times in a row returns &lt;code&gt;423&lt;/code&gt; (locked) on the 6th attempt&lt;/li&gt;
&lt;li&gt;A request with an expired token returns &lt;code&gt;401&lt;/code&gt;, not a silent failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That third one matters more than it looks. A refactor that accidentally changes the lockout threshold, or breaks it entirely, is exactly the kind of regression that a UI-only test suite tends to miss — because the UI still &lt;em&gt;looks&lt;/em&gt; fine right up until someone tries to actually break in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: What to automate vs. keep manual
&lt;/h2&gt;

&lt;p&gt;Not everything here deserves the same treatment.&lt;/p&gt;

&lt;p&gt;The happy path and the lockout boundary are stable, high-value, and get exercised on every release — strong candidates for automation at both the API and UI level. A minimal Page Object for this might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pages/login_page.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.common.by&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;By&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pages.base_page&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BasePage&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BasePage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;EMAIL_INPUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;PASSWORD_INPUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;LOGIN_BUTTON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSS_SELECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;button[type=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;submit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ERROR_MESSAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLASS_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error-message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EMAIL_INPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PASSWORD_INPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOGIN_BUTTON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_error_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ERROR_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/test_login.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_account_locks_after_5_attempts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;login_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LoginPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;login_page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong-password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;login_page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct-password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;login_page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_error_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile, usability observations — is the lockout message actually clear to a first-time user? — and one-off exploratory checks are better left manual. They rely on human judgment, not a pass/fail assertion, and automating them just gives you a brittle test that doesn't actually check the thing you care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Prioritizing under time pressure
&lt;/h2&gt;

&lt;p&gt;If a release is tomorrow and there's only time for a fraction of this list, the lockout boundary and the API-level auth checks go first. They protect account security, and they're exactly the kind of thing that quietly breaks when someone refactors the login service — and exactly the kind of bug that reaches production unnoticed if the only thing being checked is the UI happy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern, generalized
&lt;/h2&gt;

&lt;p&gt;Strip away the login specifics and the shape underneath is reusable for almost any feature:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Design test cases across happy path, boundaries, and negative cases&lt;/li&gt;
&lt;li&gt;Identify what can be verified faster and more reliably at the API layer&lt;/li&gt;
&lt;li&gt;Decide what's actually worth automating vs. what needs a human's judgment&lt;/li&gt;
&lt;li&gt;When time is short, prioritize by what protects the business most, not what's easiest to test&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the difference between "I know some testing techniques" and "I can design a test strategy for a real feature" — and it's the second one that actually gets asked about in interviews.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This walkthrough is adapted from a chapter in &lt;a href="https://singeraleksandr.gumroad.com/l/breaking-into-software-qa?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=launch" rel="noopener noreferrer"&gt;Breaking Into Software QA&lt;/a&gt;, a complete guide to breaking into software QA — manual and automated testing, API testing, mobile/Appium, real working automation code, and a full job search roadmap.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>career</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
