Skip to content

Commit 087bca6

Browse files
Steffan153hobovsky
andauthored
Ruby: Authoring kata (#252)
* Add Ruby authoring kata * Update content/languages/ruby/authoring.md * Update content/languages/ruby/authoring.md * Update content/languages/ruby/authoring.md * Update content/languages/ruby/authoring.md Co-authored-by: hobovsky <hobson@wp.pl> * add example test suite Co-authored-by: hobovsky <hobson@wp.pl>
1 parent f49c228 commit 087bca6

File tree

2 files changed

+12407
-11248
lines changed

2 files changed

+12407
-11248
lines changed
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
---
2+
kind: tutorial
3+
languages: [ruby]
4+
sidebar: "language:ruby"
5+
prev: /languages/ruby/codewars-test/
6+
---
7+
8+
# Ruby: creating and translating a kata
9+
10+
This article is intended for kata authors and translators who would like to create new content in Ruby. It attempts to explain how to create and organize things in a way conforming to [authoring guidelines](/authoring/guidelines/), shows the most common pitfalls, and how to avoid them.
11+
12+
This article is not a standalone tutorial on creating kata or translations. It's meant to be a complementary, Ruby-specific part of a more general set of HOWTOs and guidelines related to [content authoring](/authoring/). If you are going to create a Ruby translation or a new Ruby kata from scratch, please make yourself familiar with the previously mentioned documents related to authoring in general first.
13+
14+
## General info
15+
16+
Any technical information related to the Ruby setup on Codewars can be found on the [Ruby reference](/languages/ruby/) page (language versions, available gems, and setup of the code runner).
17+
18+
## Description
19+
20+
Ruby code blocks can be inserted with Ruby-specific part in [sequential code blocks](/references/markdown/extensions/#sequential-code-blocks):
21+
22+
~~~
23+
```ruby
24+
25+
...your code here...
26+
27+
```
28+
~~~
29+
30+
Ruby-specific paragraphs can be inserted with [language conditional rendering](/references/markdown/extensions/#conditional-rendering):
31+
32+
```
33+
~~~if:ruby
34+
35+
...text visible only for Ruby description...
36+
37+
~~~
38+
39+
~~~if-not:ruby
40+
41+
...text not visible in Ruby description...
42+
43+
~~~
44+
```
45+
46+
## Tasks and Requirements
47+
48+
Some concepts don't always translate well to or from Ruby. Because of this, some constructs should be avoided and some translations just shouldn't be done.
49+
- Avoid returning different data types depending on the situation (_"Return the result, or the string 'Error' if no result can be found..."_). Ruby is dynamically typed, which is not the case for some other languages. Returning `nil` might be appropriate in some situations, but throwing an error might be better in others.
50+
51+
Some kata just should not be translated into Ruby because it can be difficult to keep their initial idea:
52+
- Some kata might be meant for another language specifically. For example, a kata about `Python: Dunder Methods` should probably not be translated to Ruby, as Ruby doesn't have dunder methods, even though similar goal can be achieved with operator overloading and such.
53+
- The Ruby standard library is very rich and has many utilities available (e.g. `rotate`, `flatten`, `transpose`, `Prime`, etc.), so some non-trivial requirements in other languages could become trivial in Ruby.
54+
- Ruby supports big integers natively, so kata that rely on the implementation of arbitrary precision integer arithmetic would become trivial in Ruby.
55+
56+
## Coding
57+
58+
### Code style
59+
60+
Ruby code should stick to generally recognized Ruby conventions, with [RoboCop](https://rubystyle.guide/) being most widely accepted.
61+
62+
## Tests
63+
64+
### Testing framework
65+
66+
Ruby kata should use the [Codewars Ruby testing framework](/languages/ruby/codewars-test/) to implement and execute tests.
67+
68+
<!-- You should read its reference page to find out how to use `describe` and `it` blocks for [organization and grouping](/languages/ruby/codewars-test/#organization-of-tests), what [assertions](/languages/ruby/codewars-test/#assertions) are available, etc. -->
69+
<!-- TODO above, as the reference page is not done. -->
70+
71+
#### Dynamically generated test cases
72+
73+
It's possible to put `it` blocks in a loop and use them as a construct similar to parameterized test cases known from other testing frameworks, for example:
74+
75+
```ruby
76+
describe "Generated test cases" do
77+
generate_test_cases.each do |msg, input, expected|
78+
it msg { Test.assert_equals(user_solution(input), expected) }
79+
end
80+
end
81+
```
82+
83+
This technique is liked by authors familiar with testing frameworks that provide parameterized or generated test cases out of the box, like NUnit, or JUnit. However, some caution is needed when this approach is used. Test suites organized like this can become large and can flood the test output panel with many entries, making it unreadable or causing performance problems in client browsers.
84+
85+
#### Test grouping functions
86+
87+
To create and present test output, the Codewars test framework uses parameters of the `describe` and `it` functions:
88+
89+
```ruby
90+
describe 'Fixed tests' do
91+
it 'Odd numbers' do
92+
...some assertions...
93+
end
94+
95+
it 'Even numbers' do
96+
...some assertions...
97+
end
98+
end
99+
100+
describe 'Random tests' do
101+
it 'Small inputs' do
102+
...some assertions...
103+
end
104+
105+
it 'Large inputs' do
106+
...some assertions...
107+
end
108+
end
109+
```
110+
111+
#### Test feedback
112+
113+
You should notice that the Ruby testing framework produces one test output entry per assertion, so the test output panel can get very noisy.
114+
115+
### Random utilities
116+
117+
Ruby has a rich standard library, which includes some helpful functions that easily generate random integers in requested ranges, generate floating-point numbers, or sample and shuffle collections. The functions available allow for very convenient construction of various random input generators.
118+
119+
Some useful functions include:
120+
- [`rand()`](https://ruby-doc.org/core-3.0.0/Kernel.html#rand-method) - returns a random floating-point number in the range `[0.0, 1.0)`.
121+
- [`rand(num)`](https://ruby-doc.org/core-3.0.0/Kernel.html#rand-method) - returns a random number in the range `[0, num)`.
122+
- [`rand(range)`](https://ruby-doc.org/core-3.0.0/Kernel.html#rand-method) - returns a random number from `range`.
123+
- [`Array#shuffle`](https://ruby-doc.org/core-3.0.0/Array.html#method-i-shuffle) - returns the array shuffled.
124+
- [`Array#sample`](https://ruby-doc.org/core-3.0.0/Array.html#method-i-sample) - returns a random element from the array.
125+
- [`Array#sample(n)`](https://ruby-doc.org/core-3.0.0/Array.html#method-i-sample) - returns `n` unique random elements from the array.
126+
127+
### Additional gems
128+
129+
The Codewars runner provides a set of preinstalled gems, which are available not only for users solving a kata, but can be also used by authors to build tests and generators of test cases. For example, `faker` can be used to generate random names.
130+
131+
### Reference solution
132+
133+
If the test suite happens to use a reference solution to calculate expected values (which [should be avoided](/authoring/guidelines/submission-tests/#reference-solution) when possible), it must not be possible for the user to access it. To prevent this, it should be defined as a **__[lambda](https://www.rubyguides.com/2016/02/ruby-procs-and-lambdas/#What_is_a_Lambda) or [`Proc`](https://www.rubyguides.com/2016/02/ruby-procs-and-lambdas/#Lambdas_vs_Procs)__** instead of a normal function.
134+
135+
The reference solution or reference data ___must not___ be defined in the [Preloaded code](/authoring/guidelines/preloaded/).
136+
137+
### Calling assertions
138+
139+
The Ruby testing framework provides a set of useful [assertions](/languages/ruby/codewars-test/#assertions), but when used incorrectly, they can cause a series of problems:
140+
- Use of an assertion not suitable for the given case may lead to incorrect test results.
141+
- Incorrectly used assertions may produce confusing or unhelpful messages.
142+
143+
To avoid the above problems, calls to assertion functions should respect the following rules:
144+
- The expected value should be calculated _before_ invoking an assertion. The `expected` parameter passed to the assertion should not be a lambda/`Proc`/block, but a value calculated directly beforehand.
145+
- Appropriate assertion functions should be used for each given test. `Test.assert_equals` is not suitable in all situations. Use `Test.expect` for tests on boolean values, and `Test.expect_error` to test error handling.
146+
<!-- Use `Test.assert_approx_equals` for floating-point comparisons -->
147+
<!-- TODO: create snippet for `assert_approx_equals` equivalent in Ruby -->
148+
- Some additional attention should be paid to the order of parameters passed to assertion functions. It differs between various assertion libraries, and it happens to be quite often confused by authors, mixing up `actual` and `expected` in assertion messages. For the Ruby testing framework, the order is `(actual, expected)`.
149+
- One somewhat distinctive feature of Ruby assertions is that by default, a failed assertion does not cause a test case to fail early. It can lead to unexpected crashes when an actual value had already been asserted to be invalid, but the execution of the current test case was not stopped and following assertions continue to refer to it.
150+
- To avoid unexpected crashes in tests, it's recommended to perform some additional assertions before assuming that the answer returned by the user solution has some particular type, form, or value. For example, if the test suite sorts the returned array to verify its correctness, an explicit assertion should be added to check whether the returned object is really a list, and not, for example, `nil`.
151+
152+
## Example test suite
153+
154+
Below you can find an example test suite that covers most of the common scenarios mentioned in this article. Note that it does not present all possible techniques, so actual test suites can use a different structure, as long as they keep to established conventions and do not violate authoring guidelines.
155+
156+
```ruby
157+
describe 'Fixed tests' do
158+
it 'Regular cases' do
159+
Test.assert_equals(user_solution([1, 2, 3]), 6)
160+
Test.assert_equals(user_solution([2, 3]), 5)
161+
end
162+
163+
it 'Edge cases' do
164+
Test.assert_equals(user_solution([]), 0)
165+
Test.assert_equals(user_solution([2]), 2)
166+
end
167+
168+
it 'Input should not be modified' do
169+
arr = [*1..100].shuffle
170+
arr_copy = arr.dup
171+
# call user solution and ignore the result
172+
user_solution(arr_copy)
173+
# arr_copy should not be modified
174+
Test.assert_equals(arr_copy, arr, 'Input array was modified')
175+
end
176+
end
177+
178+
179+
describe 'Random tests' do
180+
# non-global reference solution
181+
reference_solution = ->(arr) do
182+
# calculate and return reference answer
183+
end
184+
185+
# generate data for test cases with small inputs
186+
# this test case generator combines a few types of input
187+
# in one collection
188+
def generate_small_inputs
189+
test_cases = []
190+
191+
# first type of input: regular array of small inputs (many of them)
192+
50.times {
193+
test_cases << generate_small_test_case
194+
}
195+
196+
# another type of input: array with potentially tricky numbers
197+
# (possibly many of them)
198+
50.times {
199+
test_cases << generate_small_tricky_test_case
200+
}
201+
202+
# potential edge case of single element array (a few of them)
203+
10.times {
204+
test_cases << generate_single_element_edge_case
205+
}
206+
207+
# another edge case: empty array
208+
# Not always necessary, usually fixed test is enough.
209+
# If present, there's no need for more than one.
210+
test_cases << []
211+
212+
# return randomly shuffled test cases
213+
test_cases.shuffle
214+
end
215+
216+
# Generator for large test cases, can be used for performance tests.
217+
# Can generate structure and types of test cases similar to the
218+
# generate_small_test_cases, but can also add more tricky cases,
219+
# or skip on edge cases if they were sufficiently tested in the smaller set.
220+
def generate_large_cases
221+
#... actual implementation
222+
end
223+
224+
it 'Small inputs' do
225+
inputs = generate_small_inputs
226+
inputs.each do |input|
227+
# call reference solution first, in separate statement.
228+
# we know it does not mutate the array, so no copy is needed
229+
expected = reference_solution.call(input)
230+
231+
# call user solution and get actual answer.
232+
# since the input is used after this call to compose
233+
# the assertion message, a copy is passed
234+
actual = user_solution(input.dup)
235+
236+
# Call assertion function.
237+
# Custom assertion message is used to help users with diagnosing failures.
238+
# Assertion message uses original, non-modified input.
239+
Test.assert_equals(actual, expected, "Input: #{input}")
240+
end
241+
end
242+
243+
it 'Large random tests' do
244+
large_inputs = generate_large_cases
245+
large_inputs.each do |input|
246+
# expected answer calculated first, on separate line
247+
expected = reference_solution.call(input)
248+
249+
# assertion message composed before the user solution has a chance
250+
# to mutate the input array
251+
message = "Invalid answer for array of length #{input.size}"
252+
253+
# actual answer calculated as second.
254+
# no copy is made because input is not used anymore
255+
Test.assert_equals(user_solution(input), expected, message)
256+
end
257+
end
258+
end
259+
```

0 commit comments

Comments
 (0)