Branching and Pull Requests

A pull request (PR) is a proposal to merge the commits of one branch into another, usually into main. Instead of pushing a change straight into the shared code, the developer opens a pull request: colleagues review the diff, automated tests run against it as checks, and the branch is merged only when both are satisfied. GitLab calls it a merge request.

For example, a developer commits a discount rule and its unit tests on the branch feature/CHK-231-bulk-discount and opens a pull request into main. Within minutes its page shows the changed lines, the reviewers and the checks — build, lint, unit tests, API smoke tests — each passed or failed.

Why it matters

Branching decides when a change meets the rest of the team’s work, and so which combination of changes a tester is actually testing. Branches that live for weeks collect conflicts that surface only at merge time, often just before a release.

A pull request is the last point at which a change is small, isolated and cheap to fix. Reading it is static testing, and its checks run static analysis and automated regression tests on every push, so a tester who takes part helps keep defects out of main.

Branching strategies: trunk-based development and feature branches

A branching strategy is a team’s agreement on which branches exist, how long they live and how work reaches the main branch, also called the trunk or mainline.

In trunk-based development everyone integrates into main at least daily, directly or through short-lived branches merged within about a day. Unfinished features are merged but hidden behind feature flags, switches that keep new code off until it is ready. This is the model behind continuous integration from an earlier lesson: changes stay small and main stays releasable.

With feature branches, each story or fix gets its own branch from main and returns through a pull request. The shorter a branch lives, the closer the team stays to continuous integration, which expects work to reach main at least daily; branches open for weeks drift away from main, and the merge becomes a risky event where several untested changes meet for the first time.

  • Release branches such as release/2.4 stabilise a version; a fix made there must also reach main, or the defect returns in the next release.
  • Heavier models such as GitFlow add a long-lived develop branch plus hotfix branches. In any model, ask which branch and which commit a build came from.

Pull requests and code review

A pull request shows, file by file, the diff of what the source branch would bring into the target branch, and collects everything around it: description, linked ticket, commits, reviewers’ comments on individual lines and check results. A draft pull request is not ready for review yet, but its checks can already run.

Each reviewer can comment, approve or request changes; the author answers by pushing new commits to the same branch, and the checks run again. Once the required approvals and checks are in place, the branch is merged as a merge commit, as one squashed commit (all its commits combined into one) or by a rebase (its commits replayed on top of the target branch), depending on the repository’s settings.

In ISTQB terms, reviewing a pull request is static testing, usually an informal review. Its known weakness is the rubber stamp: an approval given without reading. Small pull requests and a description, usually written in Markdown, that answers four questions make a real review possible:

  • What changed and why, with the ticket link.
  • Which tests were added, changed or removed, and what was checked by hand.
  • Risks: migrations, configuration, feature flags, shared code.
  • How to try it: test data, flags, a preview environment.

Try it: Text Diff Try it: Markdown Formatter

Tests as pull-request checks

Each push to a pull request can trigger the CI pipeline, and each job reports a status check: pending, passed or failed. Repository rules — branch protection on GitHub, merge checks on GitLab and Bitbucket — can make checks and approvals required, which turns them into a quality gate on the way into main.

A change goes from a short-lived branch into a pull request, where required checks and code review run side by side, and reaches the main branch only when both are satisfied.
  1. 1Branch from mainOne short-lived branch per story or fixfeature/CHK-231-bulk-discount
  2. 2Commit and pushThe change together with its tests
  3. 3Open a pull requestDiff against main, description, linked ticket, reviewers
  4. 4Required checksBuild, lint, unit and API smoke tests on the change merged with main
    at the same time
    • 5Code reviewReviewers, the tester among them, comment, approve or request changes

    Repeats: A check fails or changes are requested: push a fix to the same branch, and the checks run again

    Gate: all required checks passed and approvals given

  5. 6Merge into mainMerge commit, squash or rebase, per repository setting
  6. 7Test on mainOn a build that contains the merged change; record its commit

Illustrative: which checks are required, and how many approvals, is set per repository.

Pull-request checks should finish in minutes: build, lint, unit and component tests, and a few API or end-to-end smoke tests. Full regression usually runs after the merge or on a schedule; a later lesson covers that split.

Two details matter to a tester. The checks often run on the pull request merged with the current target branch (a GitHub Actions pull_request run checks out a temporary merge commit); if main moves on, the result can be stale, which is why some teams use a merge queue, which tests each pull request together with the ones queued ahead of it before merging. And a green check proves only what the tests assert: a skipped test, or an expected value edited to match the new output, stays green.

The first run of the discount pull request: one unit test failed, so the merge is blocked.

Pull request #482   feature/CHK-231-bulk-discount -> main
Head commit 9f3c2e1, first run

Required checks
  passed   build                  1m 12s
  passed   lint and type check    48s
  failed   unit tests             211 passed, 1 failed
  passed   API smoke tests        18 passed

Reviews    0 of 2 required approvals

Merging is blocked: 1 required check failed, 2 approvals missing.

Worked example: a green pull request to review

Story CHK-231: "Orders with a subtotal of 500.00 SAR or more get a 10% discount on the subtotal." After a fix, all four required checks on pull request #482 are green, one developer has approved, and the tester is the second reviewer.

Pull request #482   feature/CHK-231-bulk-discount -> main
Required checks: 4 of 4 passed     Approvals: 1 of 2

Description
  What:  10% discount when the subtotal is above 500.00 SAR.
  Tests: unit tests for 600.00 and 400.00. API tests unchanged.
  Risk:  none.

src/pricing/discount.ts
+  if (subtotal > 500) {
+    return round2(subtotal * 0.9);
+  }

tests/api/checkout.api.test.ts
-  expect(body.total).toBe(540.00);
+  expect(body.total).toBe(486.00);

The tester’s review comments

The tester reads the description, the tests and the diff, comments on the relevant lines and chooses "request changes":

  • On discount.ts: the story says "500.00 or more", but the description says "above" and the code says subtotal > 500, so an order of exactly 500.00 gets no discount. A defect found by reading.
  • On the unit tests: 600.00 and 400.00 pass whether the operator is > or >=. Ask for the boundary values 499.99 (no discount) and 500.00 (450.00 after the discount), as in the lesson on boundary value analysis.
  • On the API test: the expected total changed from 540.00 to 486.00, but the description says the API tests are unchanged. The value fits the new rule (540.00 minus 10%), but every changed expected value must be explained.
  • On "Risk: none": pricing affects totals, invoices and refunds. Ask whether the discount applies before or after the delivery fee and VAT, and how 10% of 500.05 is rounded.
  • After the merge, test the story on a main build that contains the merged change, and record its commit.

Try it: Boundary Value Analysis Try it: Regression Scenario Generator

Common mistakes

  • Treating a green pull request as a tested one: a skipped test or an edited expected value stays green.
  • Approving a pull request without reading the diff and the tests, so the review becomes a rubber stamp.
  • Testing a build without knowing which branch and commit it came from, then reporting a defect already fixed on main.
  • Re-running a failed check until it passes instead of finding out why; a flaky result can come from the test, the environment or a real timing defect in the product.
  • Keeping feature branches open for weeks, so the merge combines several untested changes just before a release.

The QA perspective

Ask to be added as a reviewer on the pull requests for the stories you will test. Read the description, then the tests, then the code: is every acceptance criterion covered, are boundaries and negative cases there, was any test skipped, deleted or changed, does the description match the diff? Request changes only for real problems, so your reviews keep their weight.

Agree with the team which tests are required checks and who acts when one fails. For a defect found after a merge, record the branch, commit and pull request number and link the defect to the pull request; a defect found in review is usually a review comment resolved before the merge.

Practice exercise

Review a pull request and decide whether it can merge

Your task

The required checks are the build, unit tests and API smoke tests. Story CHK-245 says: "Users can cancel an order while its status is Placed or Paid. Cancelling a Paid order refunds the full amount." The pull request below passed all its required checks.

Write your review comments, decide whether to approve, and say what you will test after the merge.

Material
Pull request #517   fix/CHK-245-cancel-order -> main
Required checks: 3 of 3 passed

Description
  What:  allow cancelling Placed and Paid orders; Paid orders are refunded.
  Tests: unit test for cancelling a Placed order.

src/orders/cancel.ts
-  if (order.status !== 'Placed') throw new CancelNotAllowed();
+  if (!['Placed', 'Paid', 'Shipped'].includes(order.status)) {
+    throw new CancelNotAllowed();
+  }
+  if (order.status === 'Paid') refund(order, order.total - order.deliveryFee);

tests/orders/cancel.test.ts
+  it('cancels a Placed order', ...);
-  it('rejects cancelling a Shipped order', ...);
Steps
  1. Compare the description with the story and the diff.
  2. List what the story requires that the tests do not cover.
  3. Check every changed or removed test.
  4. Decide, write your comments and note what you will test on main.
Show the model answer
Model answer

Request changes: the checks are green only because the tests miss what changed. The description matches the story, not the diff: it mentions neither Shipped, nor the deleted test, nor the delivery fee kept out of the refund.

Defect 1: Shipped was added to the allowed statuses, which the story does not allow, and the test rejects cancelling a Shipped order was deleted in the same pull request, so the checks still pass. Because the refund runs only for Paid, a cancelled Shipped order would not be refunded at all. Ask for Shipped to be removed and the test restored.

Defect 2: the refund is order.total - order.deliveryFee, not the full amount. Ask the product owner whether the delivery fee is refundable, then fix the code or the story.

Missing tests: cancelling a Paid order and the refunded amount; rejecting Shipped and any later status (only Placed and Paid may move to Cancelled); cancelling an order that is already cancelled.

After the merge, on a main build that contains the change: cancel a Placed and a Paid order, check the refund, try a Shipped order, record the commit and PR #517.

Frequently asked questions

Is a pull request a Git feature?

No. Git provides branches, commits and merges; the pull request is a feature of hosting platforms built on Git, such as GitHub, GitLab (where it is called a merge request), Bitbucket and Azure DevOps. Git itself has a git request-pull command, but it only prints a text request that summarises the changes and says where to pull them from, usually sent by e-mail.

Should a tester approve pull requests?

If the team adds testers as reviewers, yes, for what you can judge: how well the tests cover the acceptance criteria, the quality of the tests, testability and risk. You do not have to judge every line of production code. Say in your review what you checked, so that your approval is not read as more than it is.

Trunk-based development or feature branches: which is better for testing?

Both work when changes are small, merged within about a day, and every merge is tested automatically. Trunk-based development relies on strong automated checks and feature flags, so testers also test features with the flag on and off. Long-lived branches are the risky case in either model, because integration and its testing happen late.

References

  • About pull requestsGitHubWhat a pull request is: a proposal to merge changes from one branch into another, with a place to discuss, review and run checks before the merge.
  • Pro Git, 2nd Edition (Scott Chacon and Ben Straub)Apress · 2014The Pro Git book, free to read online: Git basics, branching and merging, distributed workflows and how Git stores its data.
  • Continuous Integration (Martin Fowler)martinfowler.com · 2024The widely referenced description of continuous integration: integrate often, keep the build self-testing and fast, and fix a broken build immediately.
  • Code Review Developer GuideGoogle (eng-practices)Google's published code review practices: what reviewers look for, the standard for approving a change and how to write review comments.

Practise with Mutqan tools

Compare two texts line by line or word by word and highlight differences.

Formatting

Perform full boundary value analysis (2-value and 3-value BVA) with equivalence classes and expected results.

Test Design

Normalise Markdown formatting (headings, lists, tables, emphasis) and preview the rendered result.

Formatting

Go deeper

Your progress on this topic

Part of the QA Roadmap