<?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: Vadim Toptunov</title>
    <description>The latest articles on DEV Community by Vadim Toptunov (@vadimtoptunov).</description>
    <link>https://dev.to/vadimtoptunov</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%2F4202835%2F07bc66ed-966a-44c4-861e-43547800409a.jpg</url>
      <title>DEV Community: Vadim Toptunov</title>
      <link>https://dev.to/vadimtoptunov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vadimtoptunov"/>
    <language>en</language>
    <item>
      <title>ChaosBank app — 131 switchable bugs to practice mobile testing on</title>
      <dc:creator>Vadim Toptunov</dc:creator>
      <pubDate>Fri, 23 Oct 2026 15:01:45 +0000</pubDate>
      <link>https://dev.to/vadimtoptunov/chaosbank-app-131-switchable-bugs-to-practice-mobile-testing-on-22ol</link>
      <guid>https://dev.to/vadimtoptunov/chaosbank-app-131-switchable-bugs-to-practice-mobile-testing-on-22ol</guid>
      <description>&lt;p&gt;For a while now I've been playing with different approaches to mobile UI test automation: plain XCUITest and Espresso/Compose, Appium, Maestro, page objects versus small DSLs, different ways of waiting and of dealing with flaky tests. Pretty quickly I ran into a boring but real problem: &lt;em&gt;what do I actually run all this against?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The open-source apps I found mostly fell into two groups. Some only exist for Android, which doesn't help much when you want to compare approaches across both platforms. Others are intentionally simple: a login screen, a list, a details page. They're great for learning how to drive a simulator, but they don't have many of the bugs I've seen cause trouble in real projects, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a loss rendered as a gain,&lt;/li&gt;
&lt;li&gt;a double-tap that sends a transfer twice,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1,000.50&lt;/code&gt; parsed as &lt;code&gt;1.0005&lt;/code&gt; because the device locale is German,&lt;/li&gt;
&lt;li&gt;a stale network response overwriting fresh state,&lt;/li&gt;
&lt;li&gt;a CVV printed right there on the card face.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fintech app seemed like a good place for that kind of bug, since money, auth, live data, network timing and security all meet on a handful of screens. So I ended up building my own. Two, actually, one per platform, where &lt;strong&gt;the bugs are the feature&lt;/strong&gt;. They're planted on purpose, written down, and you can switch each one on and off.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ChaosBank-iOS&lt;/strong&gt; — SwiftUI (plus a UIKit build): &lt;a href="https://github.com/VadimToptunov/ChaosBank-iOS" rel="noopener noreferrer"&gt;https://github.com/VadimToptunov/ChaosBank-iOS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChaosBank-Android&lt;/strong&gt; — Kotlin + Jetpack Compose (plus an XML build): &lt;a href="https://github.com/VadimToptunov/ChaosBank-Android" rel="noopener noreferrer"&gt;https://github.com/VadimToptunov/ChaosBank-Android&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are MIT-licensed. Functionally it's a small neobank with a stock/crypto broker on top, loosely in the style of Revolut and Robinhood, and it currently has &lt;strong&gt;131 named defects&lt;/strong&gt;, each with a short written exercise.&lt;/p&gt;

&lt;p&gt;I started it as a test bench for my own experiments, but it might also be useful as a practice ground if you're just getting into mobile development or mobile QA. Below is how it's put together, how I'd try using it, and a few things I'm still not sure about.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fejkz5mrhdbm8f1npzq81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fejkz5mrhdbm8f1npzq81.png" alt="ChaosBank iOS: markets" width="800" height="1739"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a working app rather than a mock-up. There's a web login (deliberately inside a &lt;code&gt;WKWebView&lt;/code&gt; / &lt;code&gt;WebView&lt;/code&gt;, more on that below), then OTP, a 6-digit passcode and Face ID. After that: a multi-currency home screen, transfers, currency exchange, transaction history with search and pagination, a markets screen with ticking prices, an order ticket, a portfolio with live P&amp;amp;L, and card controls.&lt;/p&gt;

&lt;p&gt;The Android app tries to be a 1:1 port: same screens, same defects, same locator names.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdgsx5uibvvetiic7vwq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdgsx5uibvvetiic7vwq.png" alt="ChaosBank Android: web login, home, markets" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping the correct code and the bugs apart
&lt;/h2&gt;

&lt;p&gt;This is the decision most of the project depends on.&lt;/p&gt;

&lt;p&gt;The obvious way to make a buggy app is to just write buggy code. For what I wanted, that didn't work well: you can't easily tell whether a test caught the bug or failed for some unrelated reason, and the "correct" behaviour only exists in the author's head. When comparing frameworks, I wanted to know exactly what each run was &lt;em&gt;supposed&lt;/em&gt; to catch.&lt;/p&gt;

&lt;p&gt;So the production logic is written to be correct (money in &lt;code&gt;Decimal&lt;/code&gt;, correct order pricing, correct balances), and each bug is a small override behind one check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kt"&gt;Defects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doubleCharge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// small, isolated buggy override&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// the correct reference implementation&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Defects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DefectId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doubleCharge&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// small, isolated buggy override&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this gives you is a simple way to check that a test really guards something:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the test against the &lt;code&gt;clean&lt;/code&gt; profile. It should pass.&lt;/li&gt;
&lt;li&gt;Relaunch with the defect switched on. It should fail.&lt;/li&gt;
&lt;li&gt;If it doesn't fail, the test probably isn't checking what you think it is.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's how that looks on screen. Same build, same screen. On the left is &lt;code&gt;clean&lt;/code&gt;; on the right, two security defects are on:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs257w56if52tinclrwgj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs257w56if52tinclrwgj.png" alt="Card screen, clean vs. cardNumberFullyVisible + cardCvvVisible" width="800" height="827"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one is less obvious. Have a look at TSLA:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F05u61g2kvt2a92am7z2j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F05u61g2kvt2a92am7z2j.png" alt="Portfolio, clean vs. pnlSign" width="800" height="827"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the left, TSLA is down $290.56 and shown in red. On the right (&lt;code&gt;pnlSign&lt;/code&gt;), the same loss shows up as a green &lt;strong&gt;+$290.56&lt;/strong&gt;. All the elements are still there, so a test that only checks that they exist will pass. You need an assertion on the &lt;em&gt;value&lt;/em&gt; to notice it.&lt;/p&gt;

&lt;p&gt;And one more: is the total on the right the sum of the accounts?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuztn1n3itqdny95fghrn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuztn1n3itqdny95fghrn.png" alt="Home, clean vs. homeTotalOmitsAccount" width="800" height="827"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It isn't. The account cards scroll horizontally, and the third one (GBP) is just off screen. With &lt;code&gt;homeTotalOmitsAccount&lt;/code&gt; on, the total quietly leaves it out: €16,626.11 instead of €17,379.28. A check like "the total is a positive number" won't catch that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Locators stay put
&lt;/h2&gt;

&lt;p&gt;The second rule is that &lt;strong&gt;a defect changes behaviour or values, but not the accessibility identifiers&lt;/strong&gt;. All IDs live in one file (&lt;code&gt;Core/A11y.swift&lt;/code&gt; on iOS, &lt;code&gt;core/A11y.kt&lt;/code&gt; on Android), and the strings are the same on both platforms: &lt;code&gt;card.cvv&lt;/code&gt; is &lt;code&gt;card.cvv&lt;/code&gt; everywhere.&lt;/p&gt;

&lt;p&gt;The idea is that a suite written against &lt;code&gt;clean&lt;/code&gt; should keep &lt;em&gt;finding&lt;/em&gt; its elements when defects are switched on, and fail on the &lt;em&gt;assertions&lt;/em&gt; instead.&lt;/p&gt;

&lt;p&gt;That's also what I needed for my own experiments. You can point XCUITest, Espresso/Compose, Appium or Maestro at the same screens and compare how they deal with changed behaviour, how they wait, and how readable the tests end up, without the comparison being mostly about chasing moved locators.&lt;/p&gt;

&lt;p&gt;Same bug, same ID, two platforms:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe86gtauuepvs8ckdsncy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe86gtauuepvs8ckdsncy.png" alt="The same defect on iOS and Android" width="800" height="807"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Switching bugs on
&lt;/h2&gt;

&lt;p&gt;Defects are set by configuration, so you don't need a separate build for each one. There are three ways to do it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Launch arguments.&lt;/strong&gt; This is handy for CI and quick runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# iOS&lt;/span&gt;
xcrun simctl launch booted VadimToptunov.ChaosBank &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-ChaosBankDefects&lt;/span&gt; pnlSign &lt;span class="nt"&gt;-ChaosBankStartUnlocked&lt;/span&gt; 1 &lt;span class="nt"&gt;-ChaosBankTab&lt;/span&gt; portfolio

&lt;span class="c"&gt;# Android&lt;/span&gt;
adb shell am start &lt;span class="nt"&gt;-n&lt;/span&gt; com.vadimtoptunov.chaosbank_android/.MainActivity &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; CHAOSBANK_DEFECTS pnlSign &lt;span class="nt"&gt;-e&lt;/span&gt; CHAOSBANK_START_UNLOCKED 1 &lt;span class="nt"&gt;-e&lt;/span&gt; CHAOSBANK_TAB portfolio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can enable a single defect, a whole profile (&lt;code&gt;ui&lt;/code&gt;, &lt;code&gt;validation&lt;/code&gt;, &lt;code&gt;security&lt;/code&gt;, &lt;code&gt;network&lt;/code&gt;, &lt;code&gt;flaky&lt;/code&gt;…), or a difficulty bundle (&lt;code&gt;beginner&lt;/code&gt;, &lt;code&gt;middle&lt;/code&gt;, &lt;code&gt;senior&lt;/code&gt;). &lt;code&gt;StartUnlocked&lt;/code&gt; skips the auth flow so you land straight on the screen you care about. It only changes where you start, not how the app behaves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The in-app developer menu.&lt;/strong&gt; Long-press the build badge in the top-right corner. From there you can switch profiles, toggle individual defects, simulate offline/slow/flaky networking, flip RTL, and browse the exercises.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flwit3jidvrh3hudhiz8o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flwit3jidvrh3hudhiz8o.png" alt="Developer menu" width="414" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Prebuilt variants.&lt;/strong&gt; Xcode build configurations (product flavors on Android) such as &lt;strong&gt;ChaosBank Flaky&lt;/strong&gt; install next to the clean app under their own name. That can be useful if someone wants to do manual exploratory testing without touching launch arguments.&lt;/p&gt;




&lt;h2&gt;
  
  
  The exercises
&lt;/h2&gt;

&lt;p&gt;Each defect has a matching entry in &lt;a href="https://github.com/VadimToptunov/ChaosBank-iOS/blob/main/exercises.json" rel="noopener noreferrer"&gt;&lt;code&gt;exercises.json&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IOS-MON-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Loss displayed as gain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"difficulty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"junior"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"money"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"pnlSign"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"launchArgument"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-ChaosBankDefects pnlSign"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expectedClean"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Loss shows negative / loss color."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expectedBuggy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Loss shows positive / gain color."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Open Portfolio and assert TSLA's P&amp;amp;L renders as a loss (negative, red)."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyLocators"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"portfolio.holding.TSLA.pnl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"portfolio.pnl"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right now there are 12 junior, 97 middle and 22 senior exercises across ten categories: money, state, security, UI, validation, localization, networking, accessibility, concurrency and performance. The difficulty labels are my own guess and are probably off in places.&lt;/p&gt;

&lt;p&gt;The junior ones are things like "a zero-amount transfer is allowed" or "the full card number isn't masked". The senior ones are mostly about timing: double-submits, a late response overwriting fresh state, animations that never quite settle.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you're starting out in mobile QA
&lt;/h2&gt;

&lt;p&gt;This is roughly the order I'd try things in. It's just one way to do it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. No code at first.&lt;/strong&gt; Install the &lt;code&gt;beginner&lt;/code&gt; build and poke around. Try not to read the defect list first. Write down what you find, then compare your notes with &lt;code&gt;exercises.json&lt;/code&gt;. It's a fairly honest way to see what you notice and what you miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A first automated test.&lt;/strong&gt; Pick a junior exercise and write a plain XCUITest for it. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;test_portfolio_lossIsRenderedAsLoss&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;XCUIApplication&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchArguments&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"-ChaosBankStartUnlocked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-ChaosBankTab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"portfolio"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;pnl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;descendants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;matching&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s"&gt;"portfolio.holding.TSLA.pnl"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="kt"&gt;XCTAssertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pnl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForExistence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;timeout&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="kt"&gt;XCTAssertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pnl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"+"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"A loss must not be shown as a gain: &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;pnl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or something similar with Compose on Android:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;card_doesNotShowCvv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ApplicationProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getApplicationContext&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;MainActivity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;putExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CHAOSBANK_START_UNLOCKED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;putExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CHAOSBANK_TAB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;ActivityScenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MainActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;composeRule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNodeWithTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"card.cvv"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;assertDoesNotExist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are rough sketches, not a finished suite. Run each one on &lt;code&gt;clean&lt;/code&gt; (green), then with &lt;code&gt;-ChaosBankDefects pnlSign&lt;/code&gt; or &lt;code&gt;CHAOSBANK_DEFECTS=cardCvvVisible&lt;/code&gt; (red). If it stays green, that's worth digging into.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Timing and flakiness.&lt;/strong&gt; The &lt;code&gt;flaky&lt;/code&gt; profile has race conditions pinned to a seed. It's a decent place to see why &lt;code&gt;sleep(2)&lt;/code&gt; tends to backfire. The &lt;code&gt;network&lt;/code&gt; profile plus "slow" in the dev menu is another one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Hybrid screens.&lt;/strong&gt; The web login sits inside a WebView on purpose. By default it's opaque: Appium only sees &lt;code&gt;NATIVE_APP&lt;/code&gt;, so you reach the fields through the native accessibility tree. With &lt;code&gt;-ChaosBankInspectableWeb 1&lt;/code&gt; (Android: &lt;code&gt;-e CHAOSBANK_INSPECTABLE_WEB 1&lt;/code&gt;), a &lt;code&gt;WEBVIEW_…&lt;/code&gt; context shows up and you can &lt;code&gt;switch_to.context(...)&lt;/code&gt; into it. It's a small thing, but it tends to be confusing the first time you run into it in a real app.&lt;/p&gt;

&lt;p&gt;There's also a hand-written UI suite for the iOS app: &lt;a href="https://github.com/VadimToptunov/ChaosBank-KassiOS-Tests" rel="noopener noreferrer"&gt;ChaosBank ⨯ KassiOS&lt;/a&gt;. It's another experiment of mine, a small Kaspresso-style DSL over XCUITest called &lt;a href="https://github.com/VadimToptunov/KassiOS" rel="noopener noreferrer"&gt;KassiOS&lt;/a&gt;. It passes on &lt;code&gt;clean&lt;/code&gt; and goes red on each injected defect. It might be more useful to look at after writing a few tests of your own.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you're starting out in mobile development
&lt;/h2&gt;

&lt;p&gt;The apps are also a small codebase with no third-party dependencies, so they might be worth reading. A few parts I spent the most time on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Money.&lt;/strong&gt; &lt;code&gt;Decimal&lt;/code&gt; everywhere, except for one deliberate spot: the &lt;code&gt;roundingDrift&lt;/code&gt; defect routes a single calculation through &lt;code&gt;Double&lt;/code&gt;, so you can see what that does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Determinism.&lt;/strong&gt; The simulated price feed and the race-condition "coin flips" come from a seeded RNG (SplitMix64), so a failing run can be reproduced. Live Yahoo Finance quotes are available, but opt-in, because they make tests non-deterministic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A mock backend as an actor&lt;/strong&gt;, with timeouts, retries, offline mode and out-of-order responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testable view models.&lt;/strong&gt; Logic-layer coverage is around 97% on iOS and 95% on Android, checked in CI. The UI is left to UI tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two UI stacks.&lt;/strong&gt; The UIKit (&lt;code&gt;UITableView&lt;/code&gt;) and XML (&lt;code&gt;RecyclerView&lt;/code&gt;) builds exist because some bugs only really happen there, like a recycled cell showing the previous row's amount.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One exercise: pick a defect, find its &lt;code&gt;Defects.isActive&lt;/code&gt; check, and read the correct &lt;code&gt;else&lt;/code&gt; branch next to the buggy one. Another: try adding a defect of your own. The &lt;a href="https://github.com/VadimToptunov/ChaosBank-iOS/blob/main/docs/DEVELOPERS_GUIDE.md" rel="noopener noreferrer"&gt;developer's guide&lt;/a&gt; has the steps. Note that CI compares the exercise catalogs of the two repos, so a new defect has to land on both platforms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using it for take-home assignments
&lt;/h2&gt;

&lt;p&gt;This one I didn't plan for, but it seems to fit: test assignments for hiring, or onboarding someone onto a team. Since the bug set is just configuration, you can build a variant around what your team cares about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For a security-focused role&lt;/strong&gt;, use the &lt;code&gt;security&lt;/code&gt; build: unmasked card data, an expired OTP that's still accepted, a deep link that skips the auth gate, a token in &lt;code&gt;UserDefaults&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For a team fighting flaky tests&lt;/strong&gt;, use &lt;code&gt;flaky&lt;/code&gt; or &lt;code&gt;senior&lt;/code&gt;, and ask for a suite that stays stable across, say, ten runs in a row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For a junior role&lt;/strong&gt;, use &lt;code&gt;beginner&lt;/code&gt; and ask for a written bug report before any code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For something narrower&lt;/strong&gt;, pass your own list with &lt;code&gt;-ChaosBankDefects&lt;/code&gt; / &lt;code&gt;CHAOSBANK_DEFECTS&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Profile builds install as a separate app with their own name (&lt;strong&gt;ChaosBank Security&lt;/strong&gt;, &lt;strong&gt;ChaosBank Flaky&lt;/strong&gt;…), so a candidate can get a single APK or simulator build. The &lt;code&gt;clean&lt;/code&gt; build gives the reviewer a baseline to run the submitted tests against.&lt;/p&gt;

&lt;p&gt;One caveat: the catalog is public, so a candidate can look up the defect list. I'd probably treat it as open-book and focus the review on &lt;em&gt;how&lt;/em&gt; the tests are written (waits, value assertions, how findings are explained) rather than on how many bugs were found.&lt;/p&gt;




&lt;h2&gt;
  
  
  A few things I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Locators stay put" was harder than I expected.&lt;/strong&gt; Some realistic bugs, like a recycled cell keeping a stale accessibility ID, are &lt;em&gt;about&lt;/em&gt; locators. I ended up making those separate, clearly labelled defects instead of exceptions to the rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Randomness works better as an opt-in.&lt;/strong&gt; Live prices are fun, but any assertion on a price or P&amp;amp;L stops meaning much when the number changes every second. So the seeded feed is the default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeping two codebases in sync needs automation.&lt;/strong&gt; Doing it by hand drifts. A CI script now compares the two exercise catalogs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The clean build has to really be clean.&lt;/strong&gt; If &lt;code&gt;clean&lt;/code&gt; has an accidental bug, the "green on clean, red on buggy" idea falls apart. That's the main reason the reference suite exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;iOS needs a Mac with Xcode 26+ and an iOS 17+ simulator. Android needs JDK 21 and API 29+.&lt;/li&gt;
&lt;li&gt;There's a reference UI suite for iOS, but none yet for Android. Espresso/Compose, Appium and Maestro suites are on the &lt;a href="https://github.com/VadimToptunov/ChaosBank-iOS/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;It's a sandbox. The "bank" lives in memory and the card number is a test one.&lt;/li&gt;
&lt;li&gt;I'm one person maintaining two apps, so some defects are probably less realistic than others.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;iOS app: &lt;a href="https://github.com/VadimToptunov/ChaosBank-iOS" rel="noopener noreferrer"&gt;https://github.com/VadimToptunov/ChaosBank-iOS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Android app: &lt;a href="https://github.com/VadimToptunov/ChaosBank-Android" rel="noopener noreferrer"&gt;https://github.com/VadimToptunov/ChaosBank-Android&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;User guide for QA/SDET: &lt;a href="https://github.com/VadimToptunov/ChaosBank-iOS/blob/main/docs/USER_GUIDE.md" rel="noopener noreferrer"&gt;docs/USER_GUIDE.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Reference UI suite: &lt;a href="https://github.com/VadimToptunov/ChaosBank-KassiOS-Tests" rel="noopener noreferrer"&gt;https://github.com/VadimToptunov/ChaosBank-KassiOS-Tests&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next on my list are Espresso/Compose, Appium and Maestro suites against the same screens, to see how the approaches compare in practice. If you give the apps a try, I'd be glad to hear which defects were hard to catch, which ones feel unrealistic, and which bugs from your own work are missing. Comments, issues and PRs are all welcome.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ios</category>
      <category>android</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
