Computer Programming 1 (JavaScript) • Problem Solving & Mini-Projects

Testing Web Apps — I can test a web app by trying edge cases and fixing UI/logic bugs.

Learn to break your own code on purpose so your users never have to.

A JavaScript data file with code brackets and a laptop.
Inside this lesson 10 sections 5 questions 6 figures

SECTION 1

Why Testing Exists: A Short History of Software Bugs

Every app you use — from Instagram to Google Docs — has been tested thousands of times before it reached your screen. Software testing is the practice of deliberately trying to make your program fail so you can find and fix problems before real users encounter them. The concept might seem counterintuitive — why would you try to break something you just built? — but history shows that untested software can cause real damage, from lost data to crashed spacecraft.

1947 • The First Literal 'Bug'

Grace Hopper's team found an actual moth stuck in a relay of the Harvard Mark II computer. They taped it into their logbook, coining the term debugging — a word programmers still use every day.

1996 • Ariane 5 Rocket Explosion

A $370 million rocket self-destructed 37 seconds after launch because of a single software conversion error that was never tested with the new rocket's flight data. This became a famous case study in why edge-case testing matters.

2000 • The Y2K Scare

Programmers had stored years as two digits (e.g., '99'). Billions of dollars were spent testing and fixing systems before the year 2000 to prevent worldwide failures — a massive lesson in thinking about boundary values.

2010s • Rise of Automated Testing

As web apps became more complex, developers adopted frameworks like Jest, Mocha, and Selenium to run hundreds of tests automatically every time code changes. Testing became a standard part of the development workflow.

Today • Testing in Modern JavaScript

Professional teams write tests alongside their code. Companies often require that new features include tests before they can be merged. Learning to test is now a core programming skill, not an afterthought.

These moments reveal a common thread: bugs are inevitable, but catastrophic failures are preventable. The key question this lesson addresses is: How do you systematically find the inputs and interactions that will break your web app before your users do?

SECTION 2

Core Principles of Testing Web Apps

Testing a web app isn't just randomly clicking around and hoping something breaks. Effective testing is guided by a set of principles that help you think like a detective — searching for the specific conditions that expose hidden problems. Let's break down the foundational ideas.

1

Edge Cases

Edge cases are unusual or extreme inputs that sit at the boundaries of what your app expects. Think: an empty string, a negative number, or a name with 500 characters. Most bugs hide at the edges, not in the middle.
2

UI Bugs vs. Logic Bugs

UI bugs are visual problems — a button that doesn't appear, text that overflows its container, or a form that looks broken on mobile. Logic bugs are errors in your code's reasoning — a calculation that returns the wrong answer or a condition that never triggers.
3

Manual vs. Automated Testing

Manual testing means you interact with your app like a user would, trying different inputs and checking results visually. Automated testing means writing code that checks your code — functions that run inputs and compare outputs to expected values.
4

Test Early, Test Often

The earlier you find a bug, the easier it is to fix. Testing after writing every small feature is far more effective than building the entire app and testing at the end. This principle is called incremental testing.
5

Reproducibility

A useful bug report includes exact steps to reproduce the problem. If you can't make a bug happen again on demand, it's nearly impossible to fix. Always note what you typed, clicked, or selected when you found the issue.
✦

KEY TAKEAWAY

Think of testing like proofreading an essay — but instead of checking for typos and grammar, you're checking for broken buttons and wrong calculations. Just as a good proofreader reads the essay from the reader's perspective (not the writer's), a good tester uses the app from the user's perspective, asking 'What weird thing might someone type here?' at every input field.

SECTION 3

The Testing Workflow: A Visual Guide

Testing follows a cycle: you identify what to test, run the test, observe the result, and either fix the bug or move on. The diagram below shows the complete testing workflow for a web app, from writing a feature to shipping it with confidence.

Testing Workflow for a Web App1. Write FeatureBuild the code2. Identify TestsNormal + edge cases3. Run TestsTry each input4. Observe ResultPass or fail?FAILPASS5a. Debug & FixFind the bug,change the codeRe-test5b. Ship It! ✓Feature is readyfor usersKey insight: Failed tests loop back to step 1. You keep cycling until all tests pass.
The testing workflow is a cycle. After writing a feature (step 1), you identify test cases including edge cases (step 2), run each test (step 3), and observe the result (step 4). If a test fails, you debug and fix (step 5a), then loop back and re-test. If all tests pass, your feature is ready to ship (step 5b).

Notice the loop on the left side of the diagram. When a test fails, you don't just shrug and move on — you trace the problem back to your code, fix it, and then run all your tests again. This is crucial because fixing one bug can sometimes accidentally create another. The loop continues until every test passes cleanly.

SECTION 4

How Testing Works in JavaScript

In JavaScript web apps, testing happens at multiple levels. At the simplest level, you can test by using console.log() to print values and check them visually. But the real power comes from writing structured test cases — small functions that call your code with specific inputs and check that the output matches what you expect.

The Anatomy of a Test Case

Every test case has three parts, sometimes called Arrange, Act, Assert. First, you arrange the setup — prepare the input data and any initial state. Second, you act — call the function or interact with the UI element you're testing. Third, you assert — check whether the actual result matches the expected result.

BASIC TEST PATTERN

functiontestAddition()//Arrangeleta=5,b=3;letexpected=8;//Actletresult=add(a,b);//Assertconsole.log(result===expected?′✓PASS′:′✗FAIL′);function testAddition() { // Arrange let a = 5, b = 3; let expected = 8; // Act let result = add(a, b); // Assert console.log(result === expected ? '✓ PASS' : '✗ FAIL'); }

This pattern works for any function. Replace add(a, b) with whatever function you're testing, set expected to the correct answer, and the assert line tells you if it worked.

Generating Edge Case Inputs

The hardest part of testing isn't writing the test — it's choosing which inputs to test. A technique called boundary value analysis guides this process. For any input, you test the normal case, the boundaries (minimum and maximum valid values), and just outside the boundaries (invalid values). For a text field that accepts 1–50 characters, you'd test inputs of length 0, 1, 25, 50, and 51.

EDGE CASE CATEGORIES

Empty→""∣Zero→0∣Negative→−1VeryLarge→999999∣SpecialChars→"<script>"WrongType→undefined,null,NaNEmpty → "" | Zero → 0 | Negative → −1 Very Large → 999999 | Special Chars → "<script>" Wrong Type → undefined, null, NaN

These six categories cover the most common edge cases in web apps. Whenever you build a feature that accepts user input, run through each category and ask: What happens if someone enters this?

💡

Pro Tip: console.assert()

JavaScript has a built-in assertion method: console.assert(condition, message). It prints the message only when the condition is false. This is a quick way to add lightweight tests: console.assert(add(2,3) === 5, 'add(2,3) should be 5');

SECTION 5

Classifying Bugs: UI Bugs vs. Logic Bugs

When you find something wrong in your web app, the first step is identifying what kind of bug you're dealing with. This matters because UI bugs and logic bugs require different debugging strategies. A UI bug means the code might be working correctly behind the scenes but the display is wrong. A logic bug means the underlying calculation or decision is flawed, even if the page looks fine.

UI Bugs vs. Logic Bugs — ComparisonUI BUGSWhat the user sees is wrong1Button doesn't appear / is hidden2Text overflows its container3Wrong color or font displayed4Layout breaks on small screens5Image doesn't loadFIX WITH:CSS changes, HTML structure,DevTools Inspect ElementLOGIC BUGSWhat the code computes is wrong1Wrong total in a shopping cart2If/else condition never triggers3Off-by-one error in a loop4Function returns string instead of number5Array index out of boundsFIX WITH:console.log(), step-throughdebugging, code reviewSometimes a single bug is both — wrong logic causes a UI element to display incorrectly.
This diagram contrasts the two main categories of web app bugs. UI bugs (left) are visual problems fixed with CSS and HTML tools, while logic bugs (right) are computational errors fixed by reviewing and correcting your JavaScript code.

When you encounter a bug, ask yourself: Is the right data getting to the screen in the wrong way, or is the wrong data getting to the screen? If you add console.log() statements to check the value of your variables and the values are correct, your bug is probably a UI bug. If the values themselves are wrong, you have a logic bug. This simple test saves you from looking in the wrong place.

SECTION 6

Worked Example: Testing a Tip Calculator

Let's walk through testing a simple tip calculator web app. The app has an input field for the bill amount, a dropdown for tip percentage (15%, 18%, or 20%), and a button that displays the tip amount and total. Our goal is to find and fix both UI and logic bugs by trying edge cases.

Testing & Debugging a Tip Calculator

  1. 1

    Step 1 — Test the Normal Case

    Enter a typical bill: $50.00, select 20% tip, click Calculate. We expect the tip to be $10.00 and the total to be $60.00. The app displays: Tip = $10.00, Total = $60.00.
    ✓ PASS — Normal case works correctly.
  2. 2

    Step 2 — Test Edge Case: Empty Input

    Leave the bill field empty and click Calculate. The app shows Tip = $NaN and Total = $NaN. This is a logic bug. The code does parseFloat("") which returns NaN, and the code doesn't check for this.
    ✗ FAIL — Fix: add if (isNaN(bill) || bill <= 0) { alert('Please enter a valid bill amount'); return; }
  3. 3

    Step 3 — Test Edge Case: Negative Number

    Enter −25 as the bill amount. The app calculates a tip of −$5.00. A negative tip doesn't make sense. This is the same logic bug — our validation should reject negative numbers too. The fix from Step 2 handles this because we added bill <= 0 to the check.
    ✓ PASS (after fix) — Negative input is now rejected with an alert.
  4. 4

    Step 4 — Test Edge Case: Very Large Number

    Enter 99999999.99 as the bill. The tip calculates correctly (the math works with large numbers), but the result text overflows the display box and gets cut off. This is a UI bug. The container has a fixed width that's too narrow for long dollar amounts.
    ✗ FAIL — Fix: change CSS to overflow-wrap: break-word; or increase the container width.
  5. 5

    Step 5 — Test Edge Case: Non-Numeric Input

    Enter "abc" as the bill. After our Step 2 fix, parseFloat("abc") returns NaN, which triggers our validation. The alert appears as expected.
    ✓ PASS — Non-numeric input is properly caught by our validation.
  6. 6

    Step 6 — Re-run All Tests

    After fixing the two bugs, we run all five test cases again. The normal case still works (our fixes didn't break it), empty input shows an alert, negative numbers show an alert, large numbers display correctly, and non-numeric input shows an alert. All tests pass.
    ✓ ALL TESTS PASS — The tip calculator is ready for users.
ℹ

Notice the Pattern

We found two bugs with just five test cases. The empty-input test revealed a logic bug (no validation), and the large-number test revealed a UI bug (overflow). Both are common categories that appear in almost every web app. Building a habit of testing these edge cases will catch the majority of bugs before they reach your users.

SECTION 7

Manual Testing vs. Automated Testing

So far, we've focused on manual testing — you the developer acting as a user. This is a great starting point, but as your apps grow larger, manual testing gets slow and error-prone. Professional developers use automated testing to complement manual testing. Let's compare both approaches.

FeatureManual TestingAutomated Testing
SpeedSlow — you click through each case by handFast — runs hundreds of tests in seconds
Setup effortNone — just open the app and start clickingHigh — you need to write test code first
Catches UI bugsExcellent — your eyes see visual problems instantlyLimited — harder to test visual appearance
Catches logic bugsGood, but easy to miss subtle errorsExcellent — checks exact values precisely
RepeatabilityLow — humans forget steps or get tiredHigh — runs identically every time
Best forSmall apps, visual layout, user experienceLarge apps, complex logic, regression testing

Manual vs. automated testing comparison

✦

KEY TAKEAWAY

Manual and automated testing are like spell-check and proofreading — you need both. Spell-check (automated testing) catches typos instantly and consistently, but it can't tell you if your sentence sounds awkward. Proofreading (manual testing) catches style and flow issues that tools miss. In this course, you'll mostly do manual testing, but knowing that automated testing exists prepares you for the next level.

SECTION 8

Connection to Professional Testing Practices

The manual testing skills you're learning now form the foundation for more advanced techniques used in the software industry. As you progress in your programming journey, you'll encounter formal testing frameworks that structure and automate the process. Here's how what you're learning connects to professional practices.

What You're Learning NowProfessional Version
Trying edge cases by hand in the browserUnit testing with Jest — functions test each edge case automatically
Checking if buttons and forms work correctlyEnd-to-end testing with Cypress — a robot browser clicks through your app
Using console.log() to check valuesDebugger tools — set breakpoints and step through code line by line
Asking a friend to try your appCode review — teammates read each other's code and suggest fixes before merging
Re-testing after every fixContinuous Integration (CI) — automated tests run every time code is pushed to GitHub

From beginner to professional testing practices

The key idea is that the thinking behind testing doesn't change as you advance — you still ask 'What could go wrong?' and 'What inputs haven't I tried?' The tools get more powerful, but the mindset you're building now is exactly what professional developers use every day. When you eventually learn a framework like Jest or Cypress, the transition will feel natural because you already understand why testing matters and how to choose good test cases.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

A student builds a web app that converts temperatures from Fahrenheit to Celsius. They test it with 32°F (which should give 0°C) and 212°F (which should give 100°C), and both work. They declare the app 'fully tested.' Name two edge cases they should also try, and explain why each could reveal a bug.
Show answer
Two important edge cases are: (1) an empty input field — if the student doesn't validate the input, parseFloat("") returns NaN, which would display as 'NaN°C' on the page. (2) A very large negative number like −999999 — while mathematically valid, the result might overflow the display container (a UI bug) or the student's formula might not handle negative Fahrenheit values correctly if they used absolute value somewhere by mistake (a logic bug). Testing only 'happy path' normal cases leaves many potential failures undiscovered.

Question 2: BASIC CALCULATION

A function calculateDiscount(price, percent) is supposed to return the discounted price. The code is: function calculateDiscount(price, percent) { return price - price * percent; }. A developer tests calculateDiscount(100, 0.2) and gets 80 — correct! But when a user enters 20 for 20%, the function returns −1900. What's the logic bug, and how would you fix it?
Show answer
The logic bug is that the function expects the percentage as a decimal (0.2 for 20%), but users naturally enter whole numbers (20 for 20%). When the user enters 20, the function computes 100 − 100 × 20 = 100 − 2000 = −1900. The fix is to divide the percent input by 100 inside the function: return price - price * (percent / 100);. Alternatively, you could validate and convert the input before calling the function. This is a classic edge case where the developer's assumptions about input format don't match the user's expectations.

Question 3: INTERMEDIATE

You have a to-do list app. The following function adds a task: function addTask(taskText) { let li = document.createElement('li'); li.textContent = taskText; document.getElementById('task-list').appendChild(li); }. List three edge-case inputs you would test, predict what would happen for each, and explain whether each result is a bug or acceptable behavior.
Show answer
Three edge cases to test: (1) Empty string "" — this would add an empty bullet point to the list, which is a logic bug because a blank task is meaningless. Fix: add if (taskText.trim() === '') return; at the top. (2) A very long string of 1000+ characters — the task would be added and might overflow the list container or push other elements off screen (a UI bug). Fix: truncate the text or set overflow-wrap: break-word in CSS. (3) HTML tags like <b>test</b> — since the code uses textContent (not innerHTML), the tags would be displayed as literal text, which is actually safe and acceptable behavior. This is a good security practice.

Question 4: APPLIED

You're building a quiz app that tracks a student's score. After answering 10 questions, the app shows a results screen with the message: 'You scored [X] out of 10. Your grade is [A/B/C/D/F].' The grading logic is: A ≥ 90%, B ≥ 80%, C ≥ 70%, D ≥ 60%, F < 60%. Design a complete set of test cases (at least 6) that tests both normal cases and edge cases. For each test, specify the score input and the expected grade output.
Show answer
A comprehensive test plan would include: (1) Score 10/10 = 100% → A (maximum boundary). (2) Score 9/10 = 90% → A (exact boundary for A). (3) Score 8/10 = 80% → B (exact boundary for B, also tests that 80% doesn't get an A). (4) Score 7/10 = 70% → C (exact boundary for C). (5) Score 6/10 = 60% → D (exact boundary for D). (6) Score 5/10 = 50% → F (below the D threshold). (7) Score 0/10 = 0% → F (minimum boundary). These boundary tests are crucial because if/else chains with ≥ comparisons often have off-by-one errors. You should also test: what happens if somehow the score exceeds 10 (e.g., a bug gives 11/10), and what message appears for 0/10. These edge cases verify that your grading logic handles every possible score correctly.

Question 5: CRITICAL THINKING

A classmate says: 'I tested my web app with 50 different inputs and everything works, so my app is bug-free.' Explain why this claim is problematic, even with extensive testing. Then describe a strategy for deciding when an app has been tested 'enough' even though you can't test every possible input.
Show answer
The claim is problematic because no amount of testing can prove a program is bug-free — testing can only prove the presence of bugs, not their absence. With most apps, the number of possible input combinations is essentially infinite (consider all possible strings, numbers, and interaction sequences). Even 50 tests might all be 'happy path' normal cases that miss entire categories of edge cases. A practical strategy for deciding when testing is 'enough' is to use equivalence partitioning — divide all possible inputs into groups (or partitions) that should behave the same way, then test at least one value from each group plus the boundary values between groups. For example, for an age input, you'd partition into: negative numbers, zero, valid ages (1–120), and unrealistically large numbers. Testing one value from each partition plus the boundaries (0, 1, 120, 121) gives you good coverage without infinite tests. You also want to ensure you've covered every code path — every if/else branch should be triggered by at least one test.

SUMMARY

Lesson Summary

Summary

Testing a web app means systematically trying inputs — especially edge cases like empty strings, negative numbers, and extremely large values — to find problems before your users do. Bugs fall into two categories: UI bugs (visual problems with how things appear on screen) and logic bugs (errors in your code's calculations or decisions). You can distinguish them by using console.log() to check whether the data is correct — if it is, the bug is in the display; if not, the bug is in the logic.

Every test follows the Arrange, Act, Assert pattern: set up your input, run the code, and check the output against what you expected. Use boundary value analysis to choose smart test inputs — test the minimum, maximum, and just-outside-valid-range values. Remember to re-test everything after every fix, because a change that solves one bug can introduce another. These manual testing skills are the foundation for the automated testing tools you'll encounter later in your programming journey.

Varsity Tutors • Computer Programming 1 (JavaScript) • Testing Web Apps — I can test a web app by trying edge cases and fixing UI/logic bugs.