Well, no. Of course not. But why wouldn’t you?
In the real world of software testing, the clock ticks differently for each project. Testing times can range from swift validations in mere minutes to exhaustive checks spanning hours. The scale of testing, from a few thousand tests to potentially millions, reflects the depth and breadth of our codebases. This variability introduces nuances: some test suites breeze through with a plethora of automated tests, while others grapple with complexities, such as web-based automation or inexplicable performance bottlenecks.
As projects and teams grow, testing often finds itself overshadowed by the urgency of shipping features, onboarding new team members, and navigating diverse opinions on coding practices. What may have started as a meticulous process of ensuring code quality and adherence to standards can morph into a juggling act, balancing speed with thoroughness. To improve testing efficiency, a variety of factors must be considered. While optimizing Continuous Integration (CI) systems is important, the primary culprits often lie elsewhere. Unoptimized, uncategorized, and lengthy test files are the main sources of testing delays. These files not only contribute to longer testing times but also make it challenging to locate specific tests and can lead to the creation of duplicate tests, further complicating the testing process and hindering the ability to identify and address issues effectively. In our quest for efficiency, we must also confront a common reality: many developers, including experienced ones within the Ruby on Rails community, may lack a comprehensive understanding of their test suites’ intricate mechanics. This knowledge gap not only leads to inefficiencies but also results in missed opportunities for streamlining and optimizing the testing process.
However, let’s not underestimate the power of testing. It’s not just about ticking boxes; it’s about building confidence in our code’s functionality, enhancing code comprehension, and safeguarding against unintended consequences of changes. Testing is the safety net that prevents us from stumbling into pitfalls and ensures that our code remains robust and reliable.
By embracing a culture of continuous improvement in testing practices, we pave the way for smoother development cycles, fewer regressions, and ultimately, a more resilient software product. It’s not just about the immediate gains but the long-term benefits that resonate throughout the life cycle of our projects.
Aggregation
By employing a few strategic techniques, one can significantly enhance the performance of sluggish test suites. Prior to this, delving into the practice of test aggregation was not a standard practice, yet the decision not to combine was really only aesthetically pleasing and actually caused some issues functionally. For example, when crafting a test suite for a serializer, it tends to resemble the following structure:
# spec/serializers/post_serializer_spec.rb
require 'rails_helper'
RSpec.describe PostSerializer, type: :serializer do
let(:user) { create(:user) }
let(:category) { create(:category) }
let(:post) { create(:post, user: user, category: category) }
let(:serializer) { PostSerializer.new(post) }
let(:serialization) { ActiveModelSerializers::Adapter.create(serializer) }
subject { JSON.parse(serialization.to_json) }
context 'attributes' do
it 'includes the expected attributes' do
expect(subject.keys).to match_array(['id', 'title', 'content', 'created_at', 'updated_at', 'user', 'category', 'comments', 'likes', 'tags', 'image_url', 'attachment_url'])
end
it { expect(subject['id']).to eq(post.id) }
it { expect(subject['title']).to eq(post.title) }
it { expect(subject['content']).to eq(post.content) }
it { expect(subject['created_at']).to eq(post.created_at.as_json) }
it { expect(subject['updated_at']).to eq(post.updated_at.as_json) }
end
context 'associations' do
let(:post) { create(:post, :with_comments) }
let(:comment1) { post.comments.first }
let(:comment2) { post.comments.second }
it { expect(subject['user']['id']).to eq(post.user.id) }
it { expect(subject['user']['email']).to eq(post.user.email) }
it { expect(subject['category']['id']).to eq(post.category.id) }
it { expect(subject['category']['name']).to eq(post.category.name) }
it { expect(subject['comments'].size).to eq(2) }
it { expect(subject['comments'].first['id']).to eq(comment1.id) }
it { expect(subject['comments'].last['id']).to eq(comment2.id) }
end
context 'custom attributes' do
it 'includes image_url if image attached' do
post.image.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'image.jpg')), filename: 'image.jpg', content_type: 'image/jpeg')
expect(subject['image_url']).to be_present
end
it 'includes attachment_url if attachment attached' do
post.attachment.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'document.pdf')), filename: 'document.pdf', content_type: 'application/pdf')
expect(subject['attachment_url']).to be_present
end
end
end
RubyFinished in 0.49347 seconds (files took 2.38 seconds to load)
15 examples, 0 failures
In total, there are 15 tests in this spec file. These tests cover various aspects such as checking attributes for the serializer, verifying custom methods related to images and attachment URLs, and ensuring the correct rendering of post associations. Despite the comprehensive coverage, the spec file remains concise and quick to execute, although, not optimal.
Initially, it may seem like we only create 2 posts and check the associations once, suggesting minimal impact on the test database and setup times. However, upon closer examination, the reality is quite different. The test scenario involves creating 15 posts, 29 users, 15 categories, and 14 comments, resulting in a total object count of 73. We will delve into the intricacies of how RSpec constructs objects and executes tests within the test database in an upcoming post.
Total: 73
Total top-level: 31
Total time: 00:00.250 (out of 00:01.083)
Total uniq factories: 4
total top-level total time time per call top-level time name
29 8 0.1130s 0.0039s 0.0470s user
15 8 0.0287s 0.0019s 0.0186s category
15 15 0.1844s 0.0123s 0.1844s post
14 0 0.0989s 0.0071s 0.0000s comment
RubyBy aggregating these tests, we can significantly reduce the number of objects created, thereby cutting down on execution time and the overall number of tests run, all while maintaining the same level of coverage. This optimization ensures efficient testing without compromising on the thoroughness of validation.
# spec/serializers/post_serializer_spec.rb
require 'rails_helper'
RSpec.describe PostSerializer, type: :serializer do
let(:user) { create(:user) }
let(:category) { create(:category) }
let(:post) { create(:post, user: user, category: category) }
let(:serializer) { PostSerializer.new(post) }
let(:serialization) { ActiveModelSerializers::Adapter.create(serializer) }
subject { JSON.parse(serialization.to_json) }
context 'attributes' do
it 'includes the expected attributes' do
expect(subject.keys).to match_array(['id', 'title', 'content', 'created_at', 'updated_at', 'user', 'category', 'comments', 'likes', 'tags', 'image_url', 'attachment_url'])
end
it 'includes the correct values for attributes' do
expect(subject['id']).to eq(post.id)
expect(subject['title']).to eq(post.title)
expect(subject['content']).to eq(post.content)
expect(subject['created_at']).to eq(post.created_at.as_json)
expect(subject['updated_at']).to eq(post.updated_at.as_json)
end
end
context 'associations' do
it 'includes user association' do
expect(subject['user']['id']).to eq(user.id)
expect(subject['user']['email']).to eq(user.email)
end
it 'includes category association' do
expect(subject['category']['id']).to eq(category.id)
expect(subject['category']['name']).to eq(category.name)
end
context "with comments" do
let(:comment1) { post.comments.first }
let(:comment2) { post.comments.last }
before { create_list(:comment, 2, post: post) }
it 'includes comments association' do
expect(subject['comments'].size).to eq(2)
expect(subject['comments'].first['id']).to eq(comment1.id)
expect(subject['comments'].last['id']).to eq(comment2.id)
end
end
end
context 'custom attributes' do
it 'includes image_url if image attached' do
post.image.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'image.jpg')), filename: 'image.jpg', content_type: 'image/jpeg')
expect(subject['image_url']).to be_present
end
it 'includes attachment_url if attachment attached' do
post.attachment.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'document.pdf')), filename: 'document.pdf', content_type: 'application/pdf')
expect(subject['attachment_url']).to be_present
end
end
end
RubyFinished in 0.33001 seconds (files took 3.04 seconds to load)
7 examples, 0 failures
The primary change involves consolidating the individual it { expect() }
blocks into a single it do
block. This aggregation reduces the total number of tests to 7 and the objects created to 25, resulting in a significant decrease in execution time by approximately 40%.
While the difference in time, such as 0.17 seconds in this instance, may seem minor on a small scale, the impact grows as more features and tests are added. A test suite that initially takes less than a second to run can quickly bloat to several seconds or even minutes, particularly when considering multiple test files with similar structures. This optimization becomes crucial to maintain efficient testing workflows.
Total: 25
Total top-level: 23
Total time: 00:00.117 (out of 00:01.232)
Total uniq factories: 4
total top-level total time time per call top-level time name
9 7 0.0475s 0.0053s 0.0411s user
7 7 0.0184s 0.0026s 0.0184s category
7 7 0.0407s 0.0058s 0.0407s post
2 2 0.0177s 0.0089s 0.0177s comment
RubyImplementing test aggregation without making any other changes to the file can lead to a substantial acceleration of the testing suite. When applied globally across the codebase, this optimization has the potential to greatly reduce CI time, resulting in cost savings on CI bills, shorter developer wait times, and faster hot fix deployments.
Moreover, leveraging tools like Rubocop with its autocorrect feature for this type of test optimization can streamline the effort even further. Going from a costly, tedious fix, to just about fully automated fix. For more details about the cop rule, you can find additional information here:
Leave a Reply