Testing in TeamCity
Build chains, real-time test reporting and the best flaky-test detection of any CI platform — configured as Kotlin DSL rather than clicked together.
2 min read · updated 19 September 2026
TeamCity's distinguishing feature is that it treats tests as first-class objects rather than as lines in a log. It tracks each one across every build forever, which gives it the best flakiness reporting of any platform here.
#The Kotlin DSL
Configuration lives in .teamcity/ in your repository, is reviewed like
code, and is reproducible.
// .teamcity/settings.kts
import jetbrains.buildServer.configs.kotlin.*
import jetbrains.buildServer.configs.kotlin.buildSteps.*
import jetbrains.buildServer.configs.kotlin.triggers.vcs
import jetbrains.buildServer.configs.kotlin.buildFeatures.*
version = "2024.12"
project {
buildType(Build)
buildType(UnitTests)
buildType(IntegrationTests)
buildType(E2ETests)
// The chain: Build runs once; the three test configurations then run
// in parallel against its artefacts.
sequential {
buildType(Build)
parallel {
buildType(UnitTests)
buildType(IntegrationTests)
buildType(E2ETests)
}
}
}
object Build : BuildType({
name = "Build"
vcs { root(DslContext.settingsRoot) }
steps {
script {
name = "Install and build"
scriptContent = """
npm ci
npm run build
""".trimIndent()
}
}
artifactRules = """
.next/** => build.zip
package-lock.json
""".trimIndent()
triggers { vcs { } }
})
object UnitTests : BuildType({
name = "Unit tests"
dependencies {
dependency(Build) {
snapshot { }
artifacts { artifactRules = "build.zip!** => ." }
}
}
steps {
script {
name = "Jest"
// The teamcity reporter streams results as they happen, so the
// build page fills in while the suite is still running.
scriptContent = "npm ci && npm test -- --reporters=jest-teamcity"
}
}
features {
// Flaky detection is a platform feature, not a plugin.
perfmon { }
}
})
object E2ETests : BuildType({
name = "E2E tests"
dependencies {
dependency(Build) { snapshot { }; artifacts { artifactRules = "build.zip!** => ." } }
}
steps {
script {
scriptContent = """
npm ci
npx playwright install --with-deps chromium
npx playwright test --reporter=junit,html
""".trimIndent()
}
}
features {
// Split by historical duration across four agents — better than
// splitting by file count, which is what most platforms offer.
parallelTests { numberOfBatches = 4 }
}
artifactRules = "playwright-report/** => playwright-report.zip"
})#Service messages
TeamCity's reporting protocol is plain stdout lines, which means any tool in any language can report structured results without a plugin:
echo "##teamcity[testStarted name='checkout can pay' captureStandardOutput='true']"
echo "##teamcity[testFailed name='checkout can pay' message='expected 201' details='...']"
echo "##teamcity[testFinished name='checkout can pay' duration='1450']"
# Attach a file to the build so it is one click from the failure
echo "##teamcity[publishArtifacts 'test-results/screenshots/**']"
# Surface a number on the build page and chart it over time
echo "##teamcity[buildStatisticValue key='flakyTests' value='3']"Most runners have a TeamCity reporter that emits these:
jest-teamcity, --teamcity for pytest via a plugin, mocha --reporter mocha-teamcity-reporter, and TeamCity's own JUnit and NUnit parsers.
#What it does better than everyone else
Test history. Every test has a page: pass rate, duration trend, the build it first failed in, the commit that is likely responsible. When a test starts failing, that page usually contains the answer.
Automatic flaky detection. A test alternating between pass and fail on the same code is marked flaky with no configuration. TeamCity maintains the list, so flakiness becomes a visible, sortable, assignable backlog rather than folklore.
Investigations. A failing test can be assigned to a person with a note, and it stays assigned across builds until resolved. Small feature, large effect on whether anything gets fixed.
Muting instead of skipping. A muted test still runs and still reports;
it just does not fail the build. That is far better than a skip, because
the result is still recorded and the mute is visible and revocable.
Real-time results. Failures appear while the build is running, not after it. On a twenty-minute suite that is twenty minutes of earlier feedback.
Parallel tests by duration. The built-in parallelTests feature splits
using historical timings, which produces much more even batches than the
file-count splitting most platforms offer — see
Playwright sharding for why that matters.
#Build chains
A chain is TeamCity's model and it is genuinely good: configurations depend on one another by snapshot (same source revision) and by artefact. The build runs once; the test configurations consume its output in parallel.
The practical benefits are that a re-run of one test configuration does not rebuild, and each configuration has its own history, its own flakiness profile and its own agent requirements.
#Where it fits
TeamCity is a strong fit for a JetBrains-tooled organisation, for large suites where the test history genuinely pays for itself, and for anyone who wants self-hosted CI with better ergonomics than Jenkins.
The costs are real: it is a server to run and upgrade, agents to license beyond the free tier, and the Kotlin DSL has a learning curve. For a small team on GitHub, GitHub Actions is less work for most of the benefit — minus the test analytics, which is the thing you would be choosing TeamCity for.
Common questions
- What is TeamCity's main advantage for testing?
- Test history and flaky detection. TeamCity tracks every test across every build, marks tests that alternate between pass and fail as flaky automatically, and shows results streaming in while the build is still running. No configuration is needed beyond reporting the results.
- Should I configure TeamCity through the UI or the Kotlin DSL?
- The DSL. Configuration in version control is reviewable, diffable and reproducible; clicked-together configuration drifts and cannot be recovered from a repository. TeamCity can export existing UI configuration to Kotlin to get you started.
- How does TeamCity split tests across agents?
- A parallel tests build feature splits by historical test duration across a chosen number of batches, which is a better distribution than splitting by file count. It is built in rather than something you script.
Runnable samples for this page
last test results ↗- YAML / Groovy / Kotlin
pipelines/ci-cd/teamcity
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Testing in JenkinsDeclarative pipelines, parallel stages, agent control and JUnit reporting — how to run a modern test suite on the CI server you probably inherited.
- Testing in Azure DevOps PipelinesA full YAML pipeline with stages, jobs, parallel test slicing and the best built-in test reporting of any CI platform.
- Flaky TestsWhy tests fail intermittently, the six root causes and how to fix each one, how to detect flakiness deliberately, and what to do with a test you cannot fix today.
- Test ReportingGetting results out of the runner and in front of people — JUnit XML, pull request annotations, per-test history, and the numbers worth publishing.
- Writing Tests in JavaJUnit 5, Mockito, AssertJ and Testcontainers — the stack that has been stable for a decade, plus Spring's test slices and what they cost.