Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Unit tests for milestone actions #425

Open
wants to merge 12 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ def self.run(params)

github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
last_stone = github_helper.get_last_milestone(repository)

UI.user_error!('No milestone found on the repository.') if last_stone.nil?
UI.user_error!("Milestone #{last_stone[:title]} has no due date.") if last_stone[:due_on].nil?
raafaelima marked this conversation as resolved.
Show resolved Hide resolved

UI.message("Last detected milestone: #{last_stone[:title]} due on #{last_stone[:due_on]}.")
milestone_duedate = last_stone[:due_on]
milestone_duration = params[:milestone_duration]
Expand Down Expand Up @@ -49,19 +53,19 @@ def self.available_options
env_name: 'GHHELPER_NEED_APPSTORE_SUBMISSION',
description: 'True if the app needs to be submitted',
optional: true,
is_string: false,
type: Boolean,
Copy link
Contributor Author

@raafaelima raafaelima Nov 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-Review: As the is_string is deprecated on the ConfigItems, I replace them with the type of variable that we want to receive. This also applies to the other places where I did that change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! It's even a small step towards addressing #278 that we opened a while ago about this 🙂

default_value: false),
FastlaneCore::ConfigItem.new(key: :milestone_duration,
env_name: 'GHHELPER_MILESTONE_DURATION',
description: 'Milestone duration in number of days',
optional: true,
is_string: false,
type: Integer,
default_value: 14),
FastlaneCore::ConfigItem.new(key: :number_of_days_from_code_freeze_to_release,
env_name: 'GHHELPER_NUMBER_OF_DAYS_FROM_CODE_FREEZE_TO_RELEASE',
description: 'Number of days from code freeze to release',
optional: true,
is_string: false,
type: Integer,
default_value: 14),
Fastlane::Helper::GithubHelper.github_token_config_item,
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def self.available_options
description: 'The GitHub milestone',
optional: false,
default_value: true,
is_string: false),
type: Boolean),
Fastlane::Helper::GithubHelper.github_token_config_item,
]
end
Expand Down
86 changes: 86 additions & 0 deletions spec/close_milestone_action_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require 'spec_helper'

describe Fastlane::Actions::CloseMilestoneAction do
let(:test_repository) { 'test-repository' }
let(:test_token) { 'Test-GithubToken-1234' }
let(:test_milestone) do
{ title: '10.1', number: '1234' }
end
let(:client) do
instance_double(
Octokit::Client,
list_milestones: [test_milestone],
update_milestone: nil,
user: instance_double('User', name: 'test'),
'auto_paginate=': nil
)
end

before do
allow(Octokit::Client).to receive(:new).and_return(client)
end

it 'properly passes the environment variable `GITHUB_TOKEN` to Octokit::Client' do
ENV['GITHUB_TOKEN'] = test_token
expect(Octokit::Client).to receive(:new).with(access_token: test_token)
run_action_without_key(:github_token)
end

it 'properly passes the parameter `:github_token` to Octokit::Client' do
expect(Octokit::Client).to receive(:new).with(access_token: test_token)
run_described_fastlane_action(default_params)
end

it 'prioritizes `:github_token` parameter over `GITHUB_TOKEN` environment variable if both are present' do
ENV['GITHUB_TOKEN'] = 'Test-EnvGithubToken-1234'
expect(Octokit::Client).to receive(:new).with(access_token: test_token)
run_described_fastlane_action(default_params)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice and imho makes sense to group those three tests into a parent describe 'GitHub Token is properly passed to the client' or something like that.

One could even consider finding a way to DRY those 3 tests so that we can easily reuse them in the tests of all the actions that rely on that github_token shared ConfigItem, since those three tests will likely always be the same implementation and logic on all those affected actions.


it 'properly passes the repository and milestone to Octokit::Client to update the milestone as closed' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it 'properly passes the repository and milestone to Octokit::Client to update the milestone as closed' do
it 'closes the expected milestone on the expected repository' do

expect(client).to receive(:update_milestone).with(test_repository, test_milestone[:number], state: 'closed')
run_described_fastlane_action(default_params)
end

it 'raises an error when the milestone is not found or does not exist' do
allow(client).to receive(:list_milestones).and_return([])
expect { run_described_fastlane_action(default_params) }.to raise_error(FastlaneCore::Interface::FastlaneError, 'Milestone 10.1 not found.')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

end

describe 'Calling the Action validates input' do
it 'raises an error if no `GITHUB_TOKEN` environment variable nor parameter `:github_token` is present' do
ENV['GITHUB_TOKEN'] = nil
expect { run_action_without_key(:github_token) }.to raise_error(FastlaneCore::Interface::FastlaneError, "No value found for 'github_token'")
end

it 'raises an error if no `GHHELPER_REPOSITORY` environment variable nor parameter `:repository` is present' do
expect { run_action_without_key(:repository) }.to raise_error(FastlaneCore::Interface::FastlaneError, "No value found for 'repository'")
end

it 'raises an error if no `GHHELPER_MILESTONE` environment variable nor parameter `:milestone` is present' do
expect { run_action_without_key(:milestone) }.to raise_error(FastlaneCore::Interface::FastlaneError, "No value found for 'milestone'")
end

it 'raises an error if `milestone:` parameter is passed as Integer' do
expect { run_action_with(:milestone, 10) }.to raise_error "'milestone' value must be a String! Found Integer instead."
end
end

def run_action_without_key(key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
def run_action_without_key(key)
def run_action_without(key:)

run_described_fastlane_action(default_params.except(key))
end

def run_action_with(key, value)
values = default_params
values[key] = value
run_described_fastlane_action(values)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: why not make this more flexible by allowing it to take a **options-style Hash as parameter so you can not only have a nicer call site, but also one that looks like it uses named parameters?

Suggested change
def run_action_with(key, value)
values = default_params
values[key] = value
run_described_fastlane_action(values)
end
def run_action_with(**keys_and_values)
params = default_params.merge(keys_and_values)
run_described_fastlane_action(params)
end

Should allow the call site to look like:

run_action_with(milestone: '10')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I see that you suggested that above, but forget to change it on the newest commit, sorry for that, Already change it 👍


def default_params
{
repository: test_repository,
milestone: test_milestone[:title],
github_token: test_token
}
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, here it is! When I saw default_params mentioned above in the code, I expected it to be a let(:default_params) and thus looked for it alongside all the other let declarations at the top, instead of here at the bottom 🙃

Any reason why this is a method here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That confused me a bit tbh 🤔.

Because, If I put those in a let(:default_params) and then remove params from it, like I'm doing on the run_action_without(key), as let means that this is a constant by convention and as you stated here, that although is allowed is not right and should be avoided.

I thought if I put the parameters again as a let and then do those operations, I will be breaking the conventions again. 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

except is not a mutation method tho (unlike [])

end
160 changes: 160 additions & 0 deletions spec/create_new_milestone_action_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
require 'spec_helper'

describe Fastlane::Actions::CreateNewMilestoneAction do
let(:test_repository) { 'test-repository' }
let(:test_token) { 'Test-GithubToken-1234' }
let(:test_milestone) do
{ title: '10.1', number: '1234', due_on: '2022-10-31T07:00:00Z' }
end
let(:milestone_list) do
[
{ title: '10.2', number: '1234', due_on: '2022-10-31T12:00:00Z' },
{ title: '10.3', number: '4567', due_on: '2022-11-02T15:00:00Z' },
{ title: '10.4', number: '7890', due_on: '2022-11-04T23:59:00Z' },
]
end
let(:client) do
instance_double(
Octokit::Client,
list_milestones: [test_milestone],
create_milestone: nil,
user: instance_double('User', name: 'test'),
'auto_paginate=': nil
)
end

before do
allow(Octokit::Client).to receive(:new).and_return(client)
end

it 'computes the correct due date and milestone description' do
comment = "Code freeze: November 14, 2022\nApp Store submission: November 28, 2022\nRelease: November 28, 2022\n"
expect(client).to receive(:create_milestone).with(test_repository, '10.2', due_on: '2022-11-14T12:00:00Z', description: comment)
run_described_fastlane_action(default_params)
end

it 'removes 3 days from the AppStore submission date when `:need_appstore_submission` is true' do
comment = "Code freeze: November 14, 2022\nApp Store submission: November 25, 2022\nRelease: November 28, 2022\n"
expect(client).to receive(:create_milestone).with(test_repository, '10.2', due_on: '2022-11-14T12:00:00Z', description: comment)
run_action_with(:need_appstore_submission, true)
end

it 'uses the most recent milestone date to calculate the due date and version of new milestone' do
comment = "Code freeze: November 18, 2022\nApp Store submission: December 02, 2022\nRelease: December 02, 2022\n"
allow(client).to receive(:list_milestones).and_return(milestone_list)
expect(client).to receive(:create_milestone).with(test_repository, '10.5', due_on: '2022-11-18T12:00:00Z', description: comment)
run_described_fastlane_action(default_params)
end

it 'raises an error when the due date of milestone does not exists' do
allow(client).to receive(:list_milestones).and_return([{ title: '10.1', number: '1234' }])
expect { run_described_fastlane_action(default_params) }.to raise_error(FastlaneCore::Interface::FastlaneError, 'Milestone 10.1 has no due date.')
end

it 'raises an error when the milestone is not found or does not exist on the repository' do
allow(client).to receive(:list_milestones).and_return([])
expect { run_described_fastlane_action(default_params) }.to raise_error(FastlaneCore::Interface::FastlaneError, 'No milestone found on the repository.')
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

nit: how about grouping those in describe 'date computation is correct' (for the first 3) and describe 'raises error when it can't find last milestone' (or maybe context 'when last milestone cannot be used'?) (for the last 2) for example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah! It will be more organized 😄


describe 'initialize' do
context 'with github_token' do
it 'properly passes the environment variable `GITHUB_TOKEN` to Octokit::Client' do
ENV['GITHUB_TOKEN'] = test_token
expect(Octokit::Client).to receive(:new).with(access_token: test_token)
run_action_without_key(:github_token)
end

it 'properly passes the parameter `:github_token` to Octokit::Client' do
expect(Octokit::Client).to receive(:new).with(access_token: test_token)
run_described_fastlane_action(default_params)
end

it 'prioritizes `:github_token` parameter over `GITHUB_TOKEN` environment variable if both are present' do
ENV['GITHUB_TOKEN'] = 'Test-EnvGithubToken-1234'
expect(Octokit::Client).to receive(:new).with(access_token: test_token)
run_described_fastlane_action(default_params)
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how you grouped the three cases that all relate to checking the passing of the token to the client in a dedicated context group. Any reason why didn't you do the same for close_milestone_action_spec for consistency? (See also my other comment with suggestion to DRY that across action specs)


context 'when using default parameters' do
let(:github_helper) do
instance_double(
Fastlane::Helper::GithubHelper,
get_last_milestone: test_milestone,
create_milestone: nil
)
end

before do
allow(Fastlane::Helper::GithubHelper).to receive(:new).and_return(github_helper)
end

it 'uses default value when neither `GHHELPER_NUMBER_OF_DAYS_FROM_CODE_FREEZE_TO_RELEASE` environment variable nor parameter `:number_of_days_from_code_freeze_to_release` is present' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gosh we really need to refactor the API of this action to use nicer and more comprehensible / consistent ConfigItem parameter names for that one… those are quite a mouthful currently 😅

One day… in another PR… in the far future… maybe…

default_code_freeze_days = 14
expect(github_helper).to receive(:create_milestone).with(
anything,
anything,
anything,
anything,
default_code_freeze_days,
anything
)
Comment on lines +86 to +93
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it wouldn't be the occasion to make this GithubHelper#create_milestone method start using named parameters instead of positional ones — that are even harder to identify in test cases like this amongst the other anything placeholders. That should then allow us to use .with(hash_including(code_freeze_days: 14)) instead of having to use all those anything placeholders there.

But that means a small refactoring of all the call sites though (which might be outside of this PR's scope… but depending on how big a change it would be—not sure there are that many call sites, the create_new_milestone action might even be the only one?—, that could still be worth it so we can then have both the call sites and those unit tests be way nicer…
I'll let you evaluate how much changes it would warrant to do this, and decide if it's worth it or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I see, the only places that will be affected are the GithubSpec and the create_milestone_action/Spec, I agree that this method should have the named params to "force" it to be better legible and cleaner.

However, TBH I do not feel that I should do it on this PR, as it is a bit out of scope (although is a small change). So, I'll open another PR to deal with this, and update these tests here once this new PR is merged 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: This is addressed on PR #426 😄

run_described_fastlane_action(default_params)
end

it 'uses default value when neither `GHHELPER_MILESTONE_DURATION` environment variable nor parameter `:milestone_duration` is present' do
default_milestone_duration = 14
expect(github_helper).to receive(:create_milestone).with(
anything,
anything,
anything,
default_milestone_duration,
anything,
anything
)
run_described_fastlane_action(default_params)
end
end
end

describe 'calling the action validates input' do
it 'raises an error if no `GITHUB_TOKEN` environment variable nor parameter `:github_token` is present' do
ENV['GITHUB_TOKEN'] = nil
expect { run_action_without_key(:github_token) }.to raise_error(FastlaneCore::Interface::FastlaneError, "No value found for 'github_token'")
end

it 'raises an error if no `GHHELPER_REPOSITORY` environment variable nor parameter `:repository` is present' do
expect { run_action_without_key(:repository) }.to raise_error(FastlaneCore::Interface::FastlaneError, "No value found for 'repository'")
end

it 'raises an error if `need_appstore_submission:` parameter is passed as String' do
expect { run_action_with(:need_appstore_submission, 'foo') }.to raise_error "'need_appstore_submission' value must be either `true` or `false`! Found String instead."
end

it 'raises an error if `milestone_duration:` parameter is passed as String' do
expect { run_action_with(:milestone_duration, 'foo') }.to raise_error "'milestone_duration' value must be a Integer! Found String instead."
end

it 'raises an error if `number_of_days_from_code_freeze_to_release:` parameter is passed as String' do
expect { run_action_with(:number_of_days_from_code_freeze_to_release, 'foo') }.to raise_error "'number_of_days_from_code_freeze_to_release' value must be a Integer! Found String instead."
end
end

def run_action_without_key(key)
run_described_fastlane_action(default_params.except(key))
end

def run_action_with(key, value)
values = default_params
values[key] = value
run_described_fastlane_action(values)
end

def default_params
{
repository: test_repository,
need_appstore_submission: false,
github_token: test_token
}
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestions as for the other spec — run_action_without(key:), run_action_with(**additional_keys_and_values), and let(:default_params)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can DRY those too using the shared_context, or if this will be too overkill, as we also need to have the default_params defined on another place, to share the operations with those two methods 🤔

end
102 changes: 102 additions & 0 deletions spec/setfrozentag_action_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
require 'spec_helper'

describe Fastlane::Actions::SetfrozentagAction do
let(:test_repository) { 'test-repository' }
let(:test_token) { 'Test-GithubToken-1234' }
let(:test_milestone) do
{ title: '10.1', number: '1234' }
end
let(:client) do
instance_double(
Octokit::Client,
list_milestones: [test_milestone],
update_milestone: nil,
user: instance_double('User', name: 'test'),
'auto_paginate=': nil
)
end

before do
allow(Octokit::Client).to receive(:new).and_return(client)
end

it 'properly passes the environment variable `GITHUB_TOKEN` to Octokit::Client' do
ENV['GITHUB_TOKEN'] = test_token
expect(Octokit::Client).to receive(:new).with(access_token: test_token)
run_action_without_key(:github_token)
end

it 'properly passes the parameter `:github_token` all the way to Octokit::Client' do
expect(Octokit::Client).to receive(:new).with(access_token: test_token)
run_described_fastlane_action(default_params)
end

it 'prioritizes `:github_token` parameter over `GITHUB_TOKEN` environment variable if both are present' do
ENV['GITHUB_TOKEN'] = 'Test-EnvGithubToken-1234'
expect(Octokit::Client).to receive(:new).with(access_token: test_token)
run_described_fastlane_action(default_params)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to DRY those, or at least group them in a describe given how all 3 are related


it 'raises an error when the milestone is not found or does not exist' do
allow(client).to receive(:list_milestones).and_return([])
expect { run_described_fastlane_action(default_params) }.to raise_error(FastlaneCore::Interface::FastlaneError, 'Milestone 10.1 not found.')
end

it 'freezes the milestone adding ❄️ to the title' do
expect(client).to receive(:update_milestone).with(test_repository, test_milestone[:number], title: '10.1 ❄️')
run_action_with(:freeze, true)
end

it 'does not freeze the milestone if is already frozen' do
allow(client).to receive(:list_milestones).and_return([{ title: '10.2 ❄️', number: '1234' }])
expect(client).not_to receive(:update_milestone)
run_action_with(:milestone, '10.2 ❄️')
end

it 'does not freeze the milestone if :freeze parameter is false' do
expect(client).to receive(:update_milestone).with(test_repository, test_milestone[:number], title: '10.1')
run_action_with(:freeze, false)
end
Copy link
Contributor Author

@raafaelima raafaelima Nov 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-Review: This for me is an odd behavior of this action If the :freeze parameter is false, it does not add the ❄️ symbol to the milestone title, as expected. However, it keeps calling the update_milestone to do an "unnecessary" update at that milestone.

Shouldn't in this case, the update_milestone not be called, following the same logic in place when the milestone is already frozen?

Copy link
Contributor

@AliSoftware AliSoftware Nov 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call is NOT unnecessary, as it will remove any existing ❄️ emoji from a frozen milestone title if it contains one — i.e. update milestone 20.9 ❄️ to 20.9. This is used when we finalize a release at the end of a sprint, just before closing the milestone (and freezing the next one on the next code freeze).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I didn't notice that behavior. 🤔
I add a test on it in any case, about removing any existing ❄️ emoji from a frozen milestone. 😄


describe 'Calling the Action validates input' do
it 'raises an error if no `GITHUB_TOKEN` environment variable nor parameter `:github_token` is present' do
ENV['GITHUB_TOKEN'] = nil
expect { run_action_without_key(:github_token) }.to raise_error(FastlaneCore::Interface::FastlaneError, "No value found for 'github_token'")
end

it 'raises an error if no `GHHELPER_REPOSITORY` environment variable nor parameter `:repository` is present' do
expect { run_action_without_key(:repository) }.to raise_error(FastlaneCore::Interface::FastlaneError, "No value found for 'repository'")
end

it 'raises an error if no `GHHELPER_MILESTORE` environment variable nor parameter `:milestone` is present' do
expect { run_action_without_key(:milestone) }.to raise_error(FastlaneCore::Interface::FastlaneError, "No value found for 'milestone'")
end

it 'raises an error if `:freeze` parameter is passed as String' do
expect { run_action_with(:freeze, 'foo') }.to raise_error "'freeze' value must be either `true` or `false`! Found String instead."
end

it 'raises an error if `:milestone` parameter is passed as Integer' do
expect { run_action_with(:milestone, 10) }.to raise_error "'milestone' value must be a String! Found Integer instead."
end
end

def run_action_without_key(key)
run_described_fastlane_action(default_params.except(key))
end

def run_action_with(key, value)
values = default_params
values[key] = value
run_described_fastlane_action(values)
end

def default_params
{
repository: test_repository,
milestone: test_milestone[:title],
freeze: true,
github_token: test_token
}
end
end