Popular New Releases in Code Coverage Tools
coverlet
v5.7.2
codecov-action
v3.1.0
grcov
code-coverage
v3.10.0-dev.1
coveralls.net
3.0.0
Popular Libraries in Code Coverage Tools
by coverlet-coverage csharp
2329 MIT
Cross platform code coverage for .NET
by codecov typescript
944 MIT
GitHub Action that uploads coverage to Codecov :open_umbrella:
by mozilla rust
687 MPL-2.0
Rust tool to collect and aggregate code coverage data for multiple source files
by codediodeio javascript
421
JavaScript Pro Tips - Code This, Not That
by tntim96 java
385 GPL-2.0
JSCover is a JavaScript Code Coverage Tool that measures line, branch and function coverage
by dwyl javascript
333
:checkered_flag: Learn how to use the Istanbul JavaScript Code Coverage Tool
by rabbitinaction python
330 NOASSERTION
Examples and source code from the book RabbitMQ in Action.
by cypress-io javascript
296 MIT
Saves the code coverage collected during Cypress tests
by webpack-contrib javascript
274 MIT
Istanbul Instrumenter Loader
Trending New libraries in Code Coverage Tools
by olavoparno typescript
74 MIT
Creates and updates README testing coverage badges with your json-summary
by k1LoW go
57
octocov is a toolkit for collecting code metrics (code coverage, code to test ratio and test execution time).
by cicirello python
28 MIT
Coverage badges, and pull request coverage checks, from JaCoCo reports in GitHub Actions
by mysugr swift
26 MIT
Code Coverage Converter from Xcode 11 to SonarQube
by splunk shell
25 Apache-2.0
sample app along with a CICD pipeline for testing multiple versions of splunk
by iFaxity typescript
19 MIT
A Vite plugin to instrument code for nyc/istanbul code coverage. In similar way as the Webpack Loader istanbul-instrumenter-loader. Only intended for use in development.
by bahmutov javascript
10
Utilities for checking the coverage produced by NYC against extra or missing files
by RomeoDespres python
8 MIT
Simple Python module to generate PKCE code verifier and code challenge
by maxep shell
8 MIT
Swift Package Manager Code Coverage Report
Top Authors in Code Coverage Tools
1
9 Libraries
1637
2
4 Libraries
31
3
2 Libraries
26
4
2 Libraries
55
5
2 Libraries
691
6
2 Libraries
352
7
1 Libraries
8
8
1 Libraries
13
9
1 Libraries
31
10
1 Libraries
5
1
9 Libraries
1637
2
4 Libraries
31
3
2 Libraries
26
4
2 Libraries
55
5
2 Libraries
691
6
2 Libraries
352
7
1 Libraries
8
8
1 Libraries
13
9
1 Libraries
31
10
1 Libraries
5
Trending Kits in Code Coverage Tools
No Trending Kits are available at this moment for Code Coverage Tools
Trending Discussions on Code Coverage Tools
Why is CodeCoverage.exe producing near empty .coverage Files?
GitLab Docker Runner to reuse installed software layers
Convert the last generated .Coverage into coveragexml for SonarQubee in TFS 2017
Making assertions from non-test-case classes
QUESTION
Why is CodeCoverage.exe producing near empty .coverage Files?
Asked 2022-Mar-25 at 19:29In our Jenkins pipeline, we use SonarQube to report on our code coverage. After running all of our unit/integration tests to produce the .coverage file, we need to analyze this file to create the ".coverage.coveragexml" which is ultimately what is used by SonarQube to interpret the code coverage. We do this by using the CodeCoverage.exe:
1"C:\Program Files (x86)\Microsoft Visual Studio\2022\TestAgent\Team Tools\Dynamic Code Coverage Tools\CodeCoverage.exe" analyze /output:"{somePath}\{someName}.coverage.coveragexml" "{somePath}\{someName}.coverage"
2
This command appears to be working, but when you run dir /s *.coveragexml
(within the directory), it displays something like:
1"C:\Program Files (x86)\Microsoft Visual Studio\2022\TestAgent\Team Tools\Dynamic Code Coverage Tools\CodeCoverage.exe" analyze /output:"{somePath}\{someName}.coverage.coveragexml" "{somePath}\{someName}.coverage"
2Directory of C:\jenkins\path\to\TestResults\coverageFile
303/22/2022 04:59 PM 64 ContainerAdministrator_DC420D3FA0BA_2022-03-22.16_46_43.coverage.coveragexml
41 File(s) 64 bytes
5
64 bytes is practically nothing - and I believe this is the reason why our SonarQube metrics show we have 0 coverage now.
I added the same dir
command, only this time to check for the .coverage
file(s), and those come back with only 10 bytes in them - making me think that these files are essentially empty. I saw this post that seems to be a similar issue. The accepted answer said to change the platform type from x86 to x64, but that did not work in my case.
The vstest.console command for running our tests is:
1"C:\Program Files (x86)\Microsoft Visual Studio\2022\TestAgent\Team Tools\Dynamic Code Coverage Tools\CodeCoverage.exe" analyze /output:"{somePath}\{someName}.coverage.coveragexml" "{somePath}\{someName}.coverage"
2Directory of C:\jenkins\path\to\TestResults\coverageFile
303/22/2022 04:59 PM 64 ContainerAdministrator_DC420D3FA0BA_2022-03-22.16_46_43.coverage.coveragexml
41 File(s) 64 bytes
5vstest.console /Parallel /EnableCodeCoverage /Logger:trx /Platform:x86 ".\somePath\Test.dll"
6
This issue originally started back when we made a change to our Jenkinsfile for it to use Visual Studio 2022 instead of 2019 (the base image was updated) in the command that started the CodeCoverage executable.
What could be causing the coverage files to be nearly/completely empty and how can I fix it?
ANSWER
Answered 2022-Mar-25 at 19:29It seems the base image we use must have a non-enterprise edition of the Code Coverage tools (which is a requirement). We tested our SonarQube projects commands locally using an enterprise edition of the tools (I have Visual Studio 2022 Enterprise installed on my machine), and the coverage files produced contain the correct data. However, when we used a Visual Studio Professional install, the files are empty just like our Jenkins pipeline.
As stated, this started happening when the base image was updated - in particular it was around November 8th 2021. It seems the base docker image we were using (mcr.microsoft.com/dotnet/framework/sdk:4.8-20220210-windowsservercore-ltsc2019) has the latest 2022 tools, but it must not be an enterprise edition - hence the empty files.
We switched our pipeline over to using dotCover instead to perform the analysis, which works as expected and our SonarQube coverage is back to normal.
QUESTION
GitLab Docker Runner to reuse installed software layers
Asked 2020-Jan-29 at 15:42A very typical scenario with GitLab CI is to install a few packages you need for your jobs (linters, code coverage tools, deployment-specific helpers and so on) and to then run your actual stages/steps of a building, testing and deploying your software.
The Docker runner is a very neat and clean solution, but it seems very wasteful to always run the steps that install the base software. Normally, Docker is able to cache such layers, but with the way the GitLab Docker runner works, that doesn't happen.
Do we realize that setting up another project to produce pre-configured Docker images would be one solution, but are there any better ones? Basically, what we want to say is: "If the before
section hasn't changed, you can reuse the image from last time, no need to reinstall wget
or whatever".
Any solution like that out there?
ANSWER
Answered 2020-Jan-29 at 14:23You can use the registry of your gitlab project.
eg.
1images:
2 stage: build
3 image: docker
4 services:
5 - docker:dind
6 script:
7 - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY # login
8 # pull the current image or in case the image does not exit, do not stop the script:
9 - docker pull $CI_REGISTRY_IMAGE:latest || true
10 # build with the pulled image as cache:
11 - docker build --pull --cache-from $CI_REGISTRY_IMAGE:latest -t "$CI_REGISTRY_IMAGE:latest" .
12 # push the final image:
13 - docker push "$CI_REGISTRY_IMAGE:latest"
14
15
QUESTION
Convert the last generated .Coverage into coveragexml for SonarQubee in TFS 2017
Asked 2020-Jan-29 at 09:54I am using .Net Core Test --collect "Code coverage"
to generate a coverage file, I need to convert this for sonarqube, the issue is I do not nave the name of the file thats generated as its placed in a folder with a guid name and the file name itself is a GUID all under the TestResults
folder
The following script works to convert .coverage
files into coveragexml
, but its for the whole working directory
1Get-ChildItem -Recurse -Filter "*.coverage" | % {
2$outfile = "$([System.IO.Path]::GetFileNameWithoutExtension($_.FullName)).coveragexml"
3$output = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($_.FullName), $outfile)
4"Analyse '$($_.Name)' with output '$outfile'..."
5. "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Team Tools\Dynamic Code Coverage Tools\codecoverage.exe" analyze /output:$output $_.FullName
6}
7
But this converts the whole directory and after several days there are many builds, all I want to do it convert the .coverage
file generated by the current build, so I need to be able to isolate the .coverage
files generated by the current build.
ANSWER
Answered 2020-Jan-29 at 09:52So you want to take only the last created code coverage file, you can filter the Get-ChiledItem
results to get the last one:
1Get-ChildItem -Recurse -Filter "*.coverage" | % {
2$outfile = "$([System.IO.Path]::GetFileNameWithoutExtension($_.FullName)).coveragexml"
3$output = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($_.FullName), $outfile)
4"Analyse '$($_.Name)' with output '$outfile'..."
5. "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Team Tools\Dynamic Code Coverage Tools\codecoverage.exe" analyze /output:$output $_.FullName
6}
7Get-ChildItem -Recurse -Filter "*.coverage" | sort LastWriteTime | select -last 1
8
QUESTION
Making assertions from non-test-case classes
Asked 2020-Jan-16 at 02:47I have a rails model that contains an ActiveRecord::Enum
. I have a view helper that takes a value of this enum, and returns one of several possible responses. Suppose the cases were called enum_cases
, for example:
1enum_cases = [:a, :b, :c]
2
3def foo(input)
4 case input
5 when :a then 1
6 when :b then 2
7 when :c then 3
8 else raise NotImplementedError, "Unhandled new case: #{input}"
9 end
10end
11
I want to unit-test this code. Checking the happy paths is trivial:
1enum_cases = [:a, :b, :c]
2
3def foo(input)
4 case input
5 when :a then 1
6 when :b then 2
7 when :c then 3
8 else raise NotImplementedError, "Unhandled new case: #{input}"
9 end
10end
11class FooHelperTests < ActionView::TestCase
12 test "foo handles all enum cases" do
13 assert_equal foo(:a), 1
14 assert_equal foo(:b), 2
15 assert_equal foo(:c), 3
16 assert_raises NotImplementedError do
17 foo(:d)
18 end
19 end
20end
21
However, this has a flaw. If new cases are added (e.g. :z
), foo
will raise
an error to bring our attention to it, and add it as a new case. But nothing stops you from forgetting to update the test to test for the new behaviour for :z
. Now I know that's mainly the job of code coverage tools, and we do use one, but just not to such a strict level that single-line gaps will blow up. Plus this is kind of a learning exercise, anyway.
So I amended my test:
1enum_cases = [:a, :b, :c]
2
3def foo(input)
4 case input
5 when :a then 1
6 when :b then 2
7 when :c then 3
8 else raise NotImplementedError, "Unhandled new case: #{input}"
9 end
10end
11class FooHelperTests < ActionView::TestCase
12 test "foo handles all enum cases" do
13 assert_equal foo(:a), 1
14 assert_equal foo(:b), 2
15 assert_equal foo(:c), 3
16 assert_raises NotImplementedError do
17 foo(:d)
18 end
19 end
20end
21test "foo handles all enum cases" do
22 remaining_cases = enum_cases.to_set
23
24 tester = -> (arg) do
25 remaining_cases.delete(arg)
26 foo(arg)
27 end
28
29 assert_equal tester.call(:a), 1
30 assert_equal tester.call(:b), 2
31 assert_equal tester.call(:c), 3
32 assert_raises NotImplementedError do
33 tester.call(:d)
34 end
35
36 assert_empty remaining_cases, "Not all cases were tested! Remaining: #{remaining_cases}"
37end
38
This works great, however it's got 2 responsibilities, and it's a pattern I end up copy/pasting (I have multiple functions to test like this):
- Perform the actual testing of
foo
- Do book keeping to ensure all params were exhausitvely checked.
I would like to make this test more focused by removing as much boiler plate as possible, and extracting it out to a place where it can easily be reused.
Attempted solutionIn another language, I would just extract a simple test helper:
1enum_cases = [:a, :b, :c]
2
3def foo(input)
4 case input
5 when :a then 1
6 when :b then 2
7 when :c then 3
8 else raise NotImplementedError, "Unhandled new case: #{input}"
9 end
10end
11class FooHelperTests < ActionView::TestCase
12 test "foo handles all enum cases" do
13 assert_equal foo(:a), 1
14 assert_equal foo(:b), 2
15 assert_equal foo(:c), 3
16 assert_raises NotImplementedError do
17 foo(:d)
18 end
19 end
20end
21test "foo handles all enum cases" do
22 remaining_cases = enum_cases.to_set
23
24 tester = -> (arg) do
25 remaining_cases.delete(arg)
26 foo(arg)
27 end
28
29 assert_equal tester.call(:a), 1
30 assert_equal tester.call(:b), 2
31 assert_equal tester.call(:c), 3
32 assert_raises NotImplementedError do
33 tester.call(:d)
34 end
35
36 assert_empty remaining_cases, "Not all cases were tested! Remaining: #{remaining_cases}"
37end
38class ExhaustivityChecker
39 def initialize(all_values, proc)
40 @remaining_values = all_values.to_set
41 @proc = proc
42 end
43
44 def run(arg, allow_invalid_args: false)
45 assert @remaining_values.include?(arg) unless allow_invalid_args
46 @remaining_values.delete(arg)
47 @proc.call(arg)
48 end
49
50 def assert_all_values_checked
51 assert_empty @remaining_values, "Not all values were tested! Remaining: #{@remaining_values}"
52 end
53end
54
Which I could easily use like:
1enum_cases = [:a, :b, :c]
2
3def foo(input)
4 case input
5 when :a then 1
6 when :b then 2
7 when :c then 3
8 else raise NotImplementedError, "Unhandled new case: #{input}"
9 end
10end
11class FooHelperTests < ActionView::TestCase
12 test "foo handles all enum cases" do
13 assert_equal foo(:a), 1
14 assert_equal foo(:b), 2
15 assert_equal foo(:c), 3
16 assert_raises NotImplementedError do
17 foo(:d)
18 end
19 end
20end
21test "foo handles all enum cases" do
22 remaining_cases = enum_cases.to_set
23
24 tester = -> (arg) do
25 remaining_cases.delete(arg)
26 foo(arg)
27 end
28
29 assert_equal tester.call(:a), 1
30 assert_equal tester.call(:b), 2
31 assert_equal tester.call(:c), 3
32 assert_raises NotImplementedError do
33 tester.call(:d)
34 end
35
36 assert_empty remaining_cases, "Not all cases were tested! Remaining: #{remaining_cases}"
37end
38class ExhaustivityChecker
39 def initialize(all_values, proc)
40 @remaining_values = all_values.to_set
41 @proc = proc
42 end
43
44 def run(arg, allow_invalid_args: false)
45 assert @remaining_values.include?(arg) unless allow_invalid_args
46 @remaining_values.delete(arg)
47 @proc.call(arg)
48 end
49
50 def assert_all_values_checked
51 assert_empty @remaining_values, "Not all values were tested! Remaining: #{@remaining_values}"
52 end
53end
54test "foo handles all enum cases" do
55 tester = ExhaustivityChecker.new(enum_cases, -> (arg) { foo(arg) })
56
57 assert_equal tester.run(:a), 1
58 assert_equal tester.run(:b), 2
59 assert_equal tester.run(:c), 3
60 assert_raises NotImplementedError do
61 tester.run(:d, allow_invalid_args: true)
62 end
63
64 tester.assert_all_values_checked
65end
66
I could then reuse this class in other tests, just by passing it different all_values
and proc
arguments, and remembering to call assert_all_values_checked
.
However, this breaks because I can't call assert
and assert_empty
from a class that isn't a subclass of ActionView::TestCase
. Is it possible to subclass/include some class/module to gain access to these methods?
ANSWER
Answered 2020-Jan-16 at 01:36enum_cases
must be kept up to date when the production logic changes violating the DRY principle. This makes it more likely for there to be a mistake. Furthermore it is test code living in production, another red flag.
We can solve this by refactoring the case into a Hash lookup making it data driven. And also giving it a name describing what it's associated with and what it does, these are "handlers". I've also turned it into a method call making it easier to access and which will bear fruit later.
1enum_cases = [:a, :b, :c]
2
3def foo(input)
4 case input
5 when :a then 1
6 when :b then 2
7 when :c then 3
8 else raise NotImplementedError, "Unhandled new case: #{input}"
9 end
10end
11class FooHelperTests < ActionView::TestCase
12 test "foo handles all enum cases" do
13 assert_equal foo(:a), 1
14 assert_equal foo(:b), 2
15 assert_equal foo(:c), 3
16 assert_raises NotImplementedError do
17 foo(:d)
18 end
19 end
20end
21test "foo handles all enum cases" do
22 remaining_cases = enum_cases.to_set
23
24 tester = -> (arg) do
25 remaining_cases.delete(arg)
26 foo(arg)
27 end
28
29 assert_equal tester.call(:a), 1
30 assert_equal tester.call(:b), 2
31 assert_equal tester.call(:c), 3
32 assert_raises NotImplementedError do
33 tester.call(:d)
34 end
35
36 assert_empty remaining_cases, "Not all cases were tested! Remaining: #{remaining_cases}"
37end
38class ExhaustivityChecker
39 def initialize(all_values, proc)
40 @remaining_values = all_values.to_set
41 @proc = proc
42 end
43
44 def run(arg, allow_invalid_args: false)
45 assert @remaining_values.include?(arg) unless allow_invalid_args
46 @remaining_values.delete(arg)
47 @proc.call(arg)
48 end
49
50 def assert_all_values_checked
51 assert_empty @remaining_values, "Not all values were tested! Remaining: #{@remaining_values}"
52 end
53end
54test "foo handles all enum cases" do
55 tester = ExhaustivityChecker.new(enum_cases, -> (arg) { foo(arg) })
56
57 assert_equal tester.run(:a), 1
58 assert_equal tester.run(:b), 2
59 assert_equal tester.run(:c), 3
60 assert_raises NotImplementedError do
61 tester.run(:d, allow_invalid_args: true)
62 end
63
64 tester.assert_all_values_checked
65end
66def foo_handlers
67 {
68 a: 1,
69 b: 2,
70 c: 3
71 }.freeze
72end
73
74def foo(input)
75 foo_handlers.fetch(input)
76rescue KeyError
77 raise NotImplementedError, "Unhandled new case: #{input}"
78end
79
Hash#fetch
is used to raise a KeyError
if the input is not found.
Then we can write a data driven test by looping through, not foo_handlers
, but a seemingly redundant expected
Hash defined in the tests.
1enum_cases = [:a, :b, :c]
2
3def foo(input)
4 case input
5 when :a then 1
6 when :b then 2
7 when :c then 3
8 else raise NotImplementedError, "Unhandled new case: #{input}"
9 end
10end
11class FooHelperTests < ActionView::TestCase
12 test "foo handles all enum cases" do
13 assert_equal foo(:a), 1
14 assert_equal foo(:b), 2
15 assert_equal foo(:c), 3
16 assert_raises NotImplementedError do
17 foo(:d)
18 end
19 end
20end
21test "foo handles all enum cases" do
22 remaining_cases = enum_cases.to_set
23
24 tester = -> (arg) do
25 remaining_cases.delete(arg)
26 foo(arg)
27 end
28
29 assert_equal tester.call(:a), 1
30 assert_equal tester.call(:b), 2
31 assert_equal tester.call(:c), 3
32 assert_raises NotImplementedError do
33 tester.call(:d)
34 end
35
36 assert_empty remaining_cases, "Not all cases were tested! Remaining: #{remaining_cases}"
37end
38class ExhaustivityChecker
39 def initialize(all_values, proc)
40 @remaining_values = all_values.to_set
41 @proc = proc
42 end
43
44 def run(arg, allow_invalid_args: false)
45 assert @remaining_values.include?(arg) unless allow_invalid_args
46 @remaining_values.delete(arg)
47 @proc.call(arg)
48 end
49
50 def assert_all_values_checked
51 assert_empty @remaining_values, "Not all values were tested! Remaining: #{@remaining_values}"
52 end
53end
54test "foo handles all enum cases" do
55 tester = ExhaustivityChecker.new(enum_cases, -> (arg) { foo(arg) })
56
57 assert_equal tester.run(:a), 1
58 assert_equal tester.run(:b), 2
59 assert_equal tester.run(:c), 3
60 assert_raises NotImplementedError do
61 tester.run(:d, allow_invalid_args: true)
62 end
63
64 tester.assert_all_values_checked
65end
66def foo_handlers
67 {
68 a: 1,
69 b: 2,
70 c: 3
71 }.freeze
72end
73
74def foo(input)
75 foo_handlers.fetch(input)
76rescue KeyError
77 raise NotImplementedError, "Unhandled new case: #{input}"
78end
79class FooHelperTests < ActionView::TestCase
80 test "foo handles all expected inputs" do
81 expected = {
82 a: 1,
83 b: 2,
84 c: 3
85 }.freeze
86
87 # Verify expect has all the cases.
88 assert_equal expect.keys.sort, foo_handlers.keys.sort
89
90 # Drive the test with the expected results, not with the production data.
91 expected.keys do |key|
92 # Again, using `fetch` to get a clear KeyError rather than nil.
93 assert_equal foo(key), expected.fetch(value)
94 end
95 end
96
97 # Simplify the tests by separating happy path from error path.
98 test "foo raises NotImplementedError if the input is not handled" do
99 assert_raises NotImplementedError do
100 # Use something that obviously does not exist to future proof the test.
101 foo(:does_not_exist)
102 end
103 end
104end
105
The redundancy between expected
and foo_handlers
is by design. You still need to change the pairs in both places, there's no way around that, but now you'll always get a failure when foo_handlers
changes but the tests do not.
- When a new key/value pair is added to
foo_handlers
the test will fail. - If a key is missing from
expected
the test will fail. - If someone accidentally wipes out
foo_handlers
the test will fail. - If the values in
foo_handlers
are wrong, the test will fail. - If the logic of
foo
is broken, the test will fail.
Initially you're just going to copy foo_handlers
into expected
. After that it becomes a regression test testing that the code still works even after refactoring. Future changes will incrementally change foo_handlers
and expected
.
But wait, there's more! Code which is hard to test is probably hard to use. Conversely, code which is easy to test is easy to use. With a few more tweaks we can use this data-driven approach to make production code more flexible.
If we make foo_handlers
an accessor with a default that comes from a method, not a constant, now we can change how foo
behaves for individual objects. This may or may not be desirable for your particular implementation, but its in your toolbox.
1enum_cases = [:a, :b, :c]
2
3def foo(input)
4 case input
5 when :a then 1
6 when :b then 2
7 when :c then 3
8 else raise NotImplementedError, "Unhandled new case: #{input}"
9 end
10end
11class FooHelperTests < ActionView::TestCase
12 test "foo handles all enum cases" do
13 assert_equal foo(:a), 1
14 assert_equal foo(:b), 2
15 assert_equal foo(:c), 3
16 assert_raises NotImplementedError do
17 foo(:d)
18 end
19 end
20end
21test "foo handles all enum cases" do
22 remaining_cases = enum_cases.to_set
23
24 tester = -> (arg) do
25 remaining_cases.delete(arg)
26 foo(arg)
27 end
28
29 assert_equal tester.call(:a), 1
30 assert_equal tester.call(:b), 2
31 assert_equal tester.call(:c), 3
32 assert_raises NotImplementedError do
33 tester.call(:d)
34 end
35
36 assert_empty remaining_cases, "Not all cases were tested! Remaining: #{remaining_cases}"
37end
38class ExhaustivityChecker
39 def initialize(all_values, proc)
40 @remaining_values = all_values.to_set
41 @proc = proc
42 end
43
44 def run(arg, allow_invalid_args: false)
45 assert @remaining_values.include?(arg) unless allow_invalid_args
46 @remaining_values.delete(arg)
47 @proc.call(arg)
48 end
49
50 def assert_all_values_checked
51 assert_empty @remaining_values, "Not all values were tested! Remaining: #{@remaining_values}"
52 end
53end
54test "foo handles all enum cases" do
55 tester = ExhaustivityChecker.new(enum_cases, -> (arg) { foo(arg) })
56
57 assert_equal tester.run(:a), 1
58 assert_equal tester.run(:b), 2
59 assert_equal tester.run(:c), 3
60 assert_raises NotImplementedError do
61 tester.run(:d, allow_invalid_args: true)
62 end
63
64 tester.assert_all_values_checked
65end
66def foo_handlers
67 {
68 a: 1,
69 b: 2,
70 c: 3
71 }.freeze
72end
73
74def foo(input)
75 foo_handlers.fetch(input)
76rescue KeyError
77 raise NotImplementedError, "Unhandled new case: #{input}"
78end
79class FooHelperTests < ActionView::TestCase
80 test "foo handles all expected inputs" do
81 expected = {
82 a: 1,
83 b: 2,
84 c: 3
85 }.freeze
86
87 # Verify expect has all the cases.
88 assert_equal expect.keys.sort, foo_handlers.keys.sort
89
90 # Drive the test with the expected results, not with the production data.
91 expected.keys do |key|
92 # Again, using `fetch` to get a clear KeyError rather than nil.
93 assert_equal foo(key), expected.fetch(value)
94 end
95 end
96
97 # Simplify the tests by separating happy path from error path.
98 test "foo raises NotImplementedError if the input is not handled" do
99 assert_raises NotImplementedError do
100 # Use something that obviously does not exist to future proof the test.
101 foo(:does_not_exist)
102 end
103 end
104end
105class Thing
106 attr_accessor :foo_handlers
107
108 # This can use a constant, as long as the method call is canonical.
109 def default_foo_handlers
110 {
111 a: 1,
112 b: 2,
113 c: 3
114 }.freeze
115 end
116
117 def initialize
118 @foo_handlers = default_foo_handlers
119 end
120
121 def foo(input)
122 foo_handlers.fetch(input)
123 rescue KeyError
124 raise NotImplementedError, "Unhandled new case: #{input}"
125 end
126end
127
Now individual objects can define their own handlers or change the values.
1enum_cases = [:a, :b, :c]
2
3def foo(input)
4 case input
5 when :a then 1
6 when :b then 2
7 when :c then 3
8 else raise NotImplementedError, "Unhandled new case: #{input}"
9 end
10end
11class FooHelperTests < ActionView::TestCase
12 test "foo handles all enum cases" do
13 assert_equal foo(:a), 1
14 assert_equal foo(:b), 2
15 assert_equal foo(:c), 3
16 assert_raises NotImplementedError do
17 foo(:d)
18 end
19 end
20end
21test "foo handles all enum cases" do
22 remaining_cases = enum_cases.to_set
23
24 tester = -> (arg) do
25 remaining_cases.delete(arg)
26 foo(arg)
27 end
28
29 assert_equal tester.call(:a), 1
30 assert_equal tester.call(:b), 2
31 assert_equal tester.call(:c), 3
32 assert_raises NotImplementedError do
33 tester.call(:d)
34 end
35
36 assert_empty remaining_cases, "Not all cases were tested! Remaining: #{remaining_cases}"
37end
38class ExhaustivityChecker
39 def initialize(all_values, proc)
40 @remaining_values = all_values.to_set
41 @proc = proc
42 end
43
44 def run(arg, allow_invalid_args: false)
45 assert @remaining_values.include?(arg) unless allow_invalid_args
46 @remaining_values.delete(arg)
47 @proc.call(arg)
48 end
49
50 def assert_all_values_checked
51 assert_empty @remaining_values, "Not all values were tested! Remaining: #{@remaining_values}"
52 end
53end
54test "foo handles all enum cases" do
55 tester = ExhaustivityChecker.new(enum_cases, -> (arg) { foo(arg) })
56
57 assert_equal tester.run(:a), 1
58 assert_equal tester.run(:b), 2
59 assert_equal tester.run(:c), 3
60 assert_raises NotImplementedError do
61 tester.run(:d, allow_invalid_args: true)
62 end
63
64 tester.assert_all_values_checked
65end
66def foo_handlers
67 {
68 a: 1,
69 b: 2,
70 c: 3
71 }.freeze
72end
73
74def foo(input)
75 foo_handlers.fetch(input)
76rescue KeyError
77 raise NotImplementedError, "Unhandled new case: #{input}"
78end
79class FooHelperTests < ActionView::TestCase
80 test "foo handles all expected inputs" do
81 expected = {
82 a: 1,
83 b: 2,
84 c: 3
85 }.freeze
86
87 # Verify expect has all the cases.
88 assert_equal expect.keys.sort, foo_handlers.keys.sort
89
90 # Drive the test with the expected results, not with the production data.
91 expected.keys do |key|
92 # Again, using `fetch` to get a clear KeyError rather than nil.
93 assert_equal foo(key), expected.fetch(value)
94 end
95 end
96
97 # Simplify the tests by separating happy path from error path.
98 test "foo raises NotImplementedError if the input is not handled" do
99 assert_raises NotImplementedError do
100 # Use something that obviously does not exist to future proof the test.
101 foo(:does_not_exist)
102 end
103 end
104end
105class Thing
106 attr_accessor :foo_handlers
107
108 # This can use a constant, as long as the method call is canonical.
109 def default_foo_handlers
110 {
111 a: 1,
112 b: 2,
113 c: 3
114 }.freeze
115 end
116
117 def initialize
118 @foo_handlers = default_foo_handlers
119 end
120
121 def foo(input)
122 foo_handlers.fetch(input)
123 rescue KeyError
124 raise NotImplementedError, "Unhandled new case: #{input}"
125 end
126end
127thing = Thing.new
128puts thing.foo(:a) # 1
129puts thing.foo(:b) # 2
130
131thing.foo_handlers = { a: 23 }
132puts thing.foo(:a) # 23
133puts thing.foo(:b) # NotImplementedError
134
And, more importantly, a subclass can change their handlers. Here we add to the handlers using Hash#merge
.
1enum_cases = [:a, :b, :c]
2
3def foo(input)
4 case input
5 when :a then 1
6 when :b then 2
7 when :c then 3
8 else raise NotImplementedError, "Unhandled new case: #{input}"
9 end
10end
11class FooHelperTests < ActionView::TestCase
12 test "foo handles all enum cases" do
13 assert_equal foo(:a), 1
14 assert_equal foo(:b), 2
15 assert_equal foo(:c), 3
16 assert_raises NotImplementedError do
17 foo(:d)
18 end
19 end
20end
21test "foo handles all enum cases" do
22 remaining_cases = enum_cases.to_set
23
24 tester = -> (arg) do
25 remaining_cases.delete(arg)
26 foo(arg)
27 end
28
29 assert_equal tester.call(:a), 1
30 assert_equal tester.call(:b), 2
31 assert_equal tester.call(:c), 3
32 assert_raises NotImplementedError do
33 tester.call(:d)
34 end
35
36 assert_empty remaining_cases, "Not all cases were tested! Remaining: #{remaining_cases}"
37end
38class ExhaustivityChecker
39 def initialize(all_values, proc)
40 @remaining_values = all_values.to_set
41 @proc = proc
42 end
43
44 def run(arg, allow_invalid_args: false)
45 assert @remaining_values.include?(arg) unless allow_invalid_args
46 @remaining_values.delete(arg)
47 @proc.call(arg)
48 end
49
50 def assert_all_values_checked
51 assert_empty @remaining_values, "Not all values were tested! Remaining: #{@remaining_values}"
52 end
53end
54test "foo handles all enum cases" do
55 tester = ExhaustivityChecker.new(enum_cases, -> (arg) { foo(arg) })
56
57 assert_equal tester.run(:a), 1
58 assert_equal tester.run(:b), 2
59 assert_equal tester.run(:c), 3
60 assert_raises NotImplementedError do
61 tester.run(:d, allow_invalid_args: true)
62 end
63
64 tester.assert_all_values_checked
65end
66def foo_handlers
67 {
68 a: 1,
69 b: 2,
70 c: 3
71 }.freeze
72end
73
74def foo(input)
75 foo_handlers.fetch(input)
76rescue KeyError
77 raise NotImplementedError, "Unhandled new case: #{input}"
78end
79class FooHelperTests < ActionView::TestCase
80 test "foo handles all expected inputs" do
81 expected = {
82 a: 1,
83 b: 2,
84 c: 3
85 }.freeze
86
87 # Verify expect has all the cases.
88 assert_equal expect.keys.sort, foo_handlers.keys.sort
89
90 # Drive the test with the expected results, not with the production data.
91 expected.keys do |key|
92 # Again, using `fetch` to get a clear KeyError rather than nil.
93 assert_equal foo(key), expected.fetch(value)
94 end
95 end
96
97 # Simplify the tests by separating happy path from error path.
98 test "foo raises NotImplementedError if the input is not handled" do
99 assert_raises NotImplementedError do
100 # Use something that obviously does not exist to future proof the test.
101 foo(:does_not_exist)
102 end
103 end
104end
105class Thing
106 attr_accessor :foo_handlers
107
108 # This can use a constant, as long as the method call is canonical.
109 def default_foo_handlers
110 {
111 a: 1,
112 b: 2,
113 c: 3
114 }.freeze
115 end
116
117 def initialize
118 @foo_handlers = default_foo_handlers
119 end
120
121 def foo(input)
122 foo_handlers.fetch(input)
123 rescue KeyError
124 raise NotImplementedError, "Unhandled new case: #{input}"
125 end
126end
127thing = Thing.new
128puts thing.foo(:a) # 1
129puts thing.foo(:b) # 2
130
131thing.foo_handlers = { a: 23 }
132puts thing.foo(:a) # 23
133puts thing.foo(:b) # NotImplementedError
134class Thing::More < Thing
135 def default_foo_handlers
136 super.merge(
137 d: 4,
138 e: 5
139 )
140 end
141end
142
143thing = Thing.new
144more = Thing::More.new
145
146puts more.foo(:d) # 4
147puts thing.foo(:d) # NotImplementedError
148
If a key requires more than a simple value, use method names and call them with Object#public_send
. Those methods can then be unit tested.
1enum_cases = [:a, :b, :c]
2
3def foo(input)
4 case input
5 when :a then 1
6 when :b then 2
7 when :c then 3
8 else raise NotImplementedError, "Unhandled new case: #{input}"
9 end
10end
11class FooHelperTests < ActionView::TestCase
12 test "foo handles all enum cases" do
13 assert_equal foo(:a), 1
14 assert_equal foo(:b), 2
15 assert_equal foo(:c), 3
16 assert_raises NotImplementedError do
17 foo(:d)
18 end
19 end
20end
21test "foo handles all enum cases" do
22 remaining_cases = enum_cases.to_set
23
24 tester = -> (arg) do
25 remaining_cases.delete(arg)
26 foo(arg)
27 end
28
29 assert_equal tester.call(:a), 1
30 assert_equal tester.call(:b), 2
31 assert_equal tester.call(:c), 3
32 assert_raises NotImplementedError do
33 tester.call(:d)
34 end
35
36 assert_empty remaining_cases, "Not all cases were tested! Remaining: #{remaining_cases}"
37end
38class ExhaustivityChecker
39 def initialize(all_values, proc)
40 @remaining_values = all_values.to_set
41 @proc = proc
42 end
43
44 def run(arg, allow_invalid_args: false)
45 assert @remaining_values.include?(arg) unless allow_invalid_args
46 @remaining_values.delete(arg)
47 @proc.call(arg)
48 end
49
50 def assert_all_values_checked
51 assert_empty @remaining_values, "Not all values were tested! Remaining: #{@remaining_values}"
52 end
53end
54test "foo handles all enum cases" do
55 tester = ExhaustivityChecker.new(enum_cases, -> (arg) { foo(arg) })
56
57 assert_equal tester.run(:a), 1
58 assert_equal tester.run(:b), 2
59 assert_equal tester.run(:c), 3
60 assert_raises NotImplementedError do
61 tester.run(:d, allow_invalid_args: true)
62 end
63
64 tester.assert_all_values_checked
65end
66def foo_handlers
67 {
68 a: 1,
69 b: 2,
70 c: 3
71 }.freeze
72end
73
74def foo(input)
75 foo_handlers.fetch(input)
76rescue KeyError
77 raise NotImplementedError, "Unhandled new case: #{input}"
78end
79class FooHelperTests < ActionView::TestCase
80 test "foo handles all expected inputs" do
81 expected = {
82 a: 1,
83 b: 2,
84 c: 3
85 }.freeze
86
87 # Verify expect has all the cases.
88 assert_equal expect.keys.sort, foo_handlers.keys.sort
89
90 # Drive the test with the expected results, not with the production data.
91 expected.keys do |key|
92 # Again, using `fetch` to get a clear KeyError rather than nil.
93 assert_equal foo(key), expected.fetch(value)
94 end
95 end
96
97 # Simplify the tests by separating happy path from error path.
98 test "foo raises NotImplementedError if the input is not handled" do
99 assert_raises NotImplementedError do
100 # Use something that obviously does not exist to future proof the test.
101 foo(:does_not_exist)
102 end
103 end
104end
105class Thing
106 attr_accessor :foo_handlers
107
108 # This can use a constant, as long as the method call is canonical.
109 def default_foo_handlers
110 {
111 a: 1,
112 b: 2,
113 c: 3
114 }.freeze
115 end
116
117 def initialize
118 @foo_handlers = default_foo_handlers
119 end
120
121 def foo(input)
122 foo_handlers.fetch(input)
123 rescue KeyError
124 raise NotImplementedError, "Unhandled new case: #{input}"
125 end
126end
127thing = Thing.new
128puts thing.foo(:a) # 1
129puts thing.foo(:b) # 2
130
131thing.foo_handlers = { a: 23 }
132puts thing.foo(:a) # 23
133puts thing.foo(:b) # NotImplementedError
134class Thing::More < Thing
135 def default_foo_handlers
136 super.merge(
137 d: 4,
138 e: 5
139 )
140 end
141end
142
143thing = Thing.new
144more = Thing::More.new
145
146puts more.foo(:d) # 4
147puts thing.foo(:d) # NotImplementedError
148def foo_handlers
149 {
150 a: :handle_a,
151 b: :handle_b,
152 c: :handle_c
153 }.freeze
154end
155
156def foo(input)
157 public_send(foo_handlers.fetch(input), input)
158rescue KeyError
159 raise NotImplementedError, "Unhandled new case: #{input}"
160end
161
162def handle_a(input)
163 ...
164end
165
166def handle_b(input)
167 ...
168end
169
170def handle_c(input)
171 ...
172end
173
Community Discussions contain sources that include Stack Exchange Network
Tutorials and Learning Resources in Code Coverage Tools
Tutorials and Learning Resources are not available at this moment for Code Coverage Tools