Kotlin / Reusable behavior and typed boundaries / Lesson 12
Organize modules and make checks deterministic
Use packages and visibility deliberately and separate pure rules from clocks, I/O, and test framework configuration.
On this page
Lesson goals
- Distinguish a package namespace from a Kotlin compilation module.
- Design meaningful checks with fixed inputs and explicit expected outcomes.
A package organizes names rather than granting access by itself
A package header assigns fully qualified names to declarations in a source file. Imports make declarations available by short name; they do not copy code or bypass visibility. File location conventions help maintainers and build tools, but the package declaration determines the namespace. Avoid assuming that two declarations in the same package automatically share privileged access. Kotlin internal refers to a compilation module, which depends on the build arrangement, not an arbitrary folder or package. Module boundaries should follow actual build and ownership needs rather than a misleading directory label.
Expose the smallest useful contract
Top-level private declarations are visible within their file. Class-member private restricts access to the class, protected supports relevant subclass access, and public is the default when no modifier changes it. internal is useful for a module’s implementation API but is not a security boundary against untrusted runtime code. Keep helpers private when callers should rely on a higher-level operation. If tests require access to every private helper, reconsider whether they are testing implementation detail instead of the intended behavior. Do not widen production APIs solely to make trivial helper assertions convenient.
package lab.booking
private fun normalized(text: String): String = text.trim()
fun validWorkshopCode(text: String): Boolean {
val value = normalized(text)
return value.startsWith("ws-") && value.length > 3
}
fun main() {
println(validWorkshopCode(" ws-7 "))
println(validWorkshopCode("ws-"))
}
true
false
Expected output is true and false. The example validates a deliberately small synthetic code format, not a production identifier standard. It is a single complete file with a package header and was not compiled.
Pass changing context into the rule
Tests become fragile when a rule reads the real clock, random source, network, or mutable singleton implicitly. Separate the pure decision from context acquisition. A cancellation rule can accept current time and deadline as values, allowing boundary cases to be checked deterministically. Use a domain-appropriate time type in an actual application; a simple integer teaching model can explain the comparison without pretending to solve zones, daylight saving, or clock synchronization. Specify whether equality permits cancellation, because that exact boundary is often where requirements remain ambiguous.
Check the business boundary rather than repeat the implementation
The example uses check on expected Boolean outcomes for before and at the deadline. Unlike JVM assert, whose execution depends on assertion enablement, check is an ordinary runtime precondition-style function that throws when false. This is a tiny standalone teaching harness, not a replacement for a project’s test framework. A real test suite should use the configured framework’s assertions and lifecycle, record toolchain versions, and avoid inventing a passing result when no tests ran. The output below is the expected result if compiled and executed successfully.
fun mayCancel(nowMinute: Int, deadlineMinute: Int): Boolean =
nowMinute < deadlineMinute
fun main() {
val cases = listOf(9 to true, 10 to false, 11 to false)
for ((now, expected) in cases) {
check(mayCancel(now, 10) == expected) { "boundary mismatch at $now" }
}
println("3 declared boundary cases passed")
}
3 declared boundary cases passed
The expected line is 3 declared boundary cases passed. Equality is deliberately rejected. This output is reasoned only: no Kotlin compiler or test runner executed in the authoring environment.
Keep build compatibility and test claims explicit
Kotlin source, compiler plugin, standard library, JVM target, Java toolchain, and test framework versions must fit the project’s declared build. Do not copy a latest-looking dependency number into an existing build without checking its compatibility policy. kotlin.test and JUnit are related through platform-specific integration, not interchangeable package names that work without configuration. This course installs no dependencies and provides no unverified build script. Preserve complete commands, environment, and observed results when a real isolated test becomes available, and keep unexecuted examples labeled as such.
Common errors and fixes
internal is expected to grant access only to one package.
- Why it happens
- Package namespace was confused with compilation module.
- How to fix it
- Review the actual module build and choose the appropriate visibility.
- Prevent it next time
- Document package and module boundaries separately.
A deadline test fails depending on the current time.
- Why it happens
- The rule reads a real clock implicitly.
- How to fix it
- Pass a clock or fixed time into the decision boundary.
- Prevent it next time
- Use deterministic fixtures for before, equal, and after cases.
A test report says passed although no compiler ran.
- Why it happens
- Expected output was mistaken for observed evidence.
- How to fix it
- Label the example unexecuted and record actual tool availability.
- Prevent it next time
- Tie each execution claim to a command and captured result.
Check your understanding
1. Should a private normalization helper become public solely to test trim?
Show worked solution
Usually test the public validation behavior with padded, empty, and malformed inputs. Exposing the helper creates an API obligation without necessarily improving confidence. If normalization becomes an independently meaningful capability, redesign its boundary deliberately and test that contract rather than widening access as a shortcut.
2. What should a truthful report say about the provided harness in this course?
Show worked solution
It is complete Kotlin source with a reasoned expected output and three declared boundary cases, but it was not compiled or executed because Kotlin tooling was unavailable. That is useful teaching material, not a recorded test pass. An actual project run must capture its own compatible toolchain and result.
Key takeaways
- Packages, modules, and visibility express different boundaries.
- Deterministic checks and honest execution records make evidence useful.