diff --git a/rubocop/cop_todo.rb b/rubocop/cop_todo.rb
new file mode 100644
index 0000000000000000000000000000000000000000..42e2f9fbe13ea35b62ba1bcda80cbd67f962d02d
--- /dev/null
+++ b/rubocop/cop_todo.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module RuboCop
+  class CopTodo
+    attr_accessor :previously_disabled
+
+    attr_reader :cop_name, :files, :offense_count
+
+    def initialize(cop_name)
+      @cop_name = cop_name
+      @files = Set.new
+      @offense_count = 0
+      @cop_class = self.class.find_cop_by_name(cop_name)
+      @previously_disabled = false
+    end
+
+    def record(file, offense_count)
+      @files << file
+      @offense_count += offense_count
+    end
+
+    def autocorrectable?
+      @cop_class&.support_autocorrect?
+    end
+
+    def to_yaml
+      yaml = []
+      yaml << '---'
+      yaml << '# Cop supports --auto-correct.' if autocorrectable?
+      yaml << "#{cop_name}:"
+
+      if previously_disabled
+        yaml << "  # Offense count: #{offense_count}"
+        yaml << '  # Temporarily disabled due to too many offenses'
+        yaml << '  Enabled: false'
+      end
+
+      yaml << '  Exclude:'
+      yaml.concat files.sort.map { |file| "    - '#{file}'" }
+      yaml << ''
+
+      yaml.join("\n")
+    end
+
+    def self.find_cop_by_name(cop_name)
+      RuboCop::Cop::Registry.global.find_by_cop_name(cop_name)
+    end
+  end
+end
diff --git a/rubocop/formatter/todo_formatter.rb b/rubocop/formatter/todo_formatter.rb
index 662cc1551ffb880fd6d513f5712b724bf0527ece..789d0418f96c1ebca4da2f44dc45029ff9f977e2 100644
--- a/rubocop/formatter/todo_formatter.rb
+++ b/rubocop/formatter/todo_formatter.rb
@@ -5,6 +5,7 @@
 require 'yaml'
 
 require_relative '../todo_dir'
+require_relative '../cop_todo'
 
 module RuboCop
   module Formatter
@@ -14,26 +15,6 @@ module Formatter
     # For example, this formatter stores offenses for `RSpec/VariableName`
     # in `.rubocop_todo/rspec/variable_name.yml`.
     class TodoFormatter < BaseFormatter
-      class Todo
-        attr_reader :cop_name, :files, :offense_count
-
-        def initialize(cop_name)
-          @cop_name = cop_name
-          @files = Set.new
-          @offense_count = 0
-          @cop_class = RuboCop::Cop::Registry.global.find_by_cop_name(cop_name)
-        end
-
-        def record(file, offense_count)
-          @files << file
-          @offense_count += offense_count
-        end
-
-        def autocorrectable?
-          @cop_class&.support_autocorrect?
-        end
-      end
-
       DEFAULT_BASE_DIRECTORY = File.expand_path('../../.rubocop_todo', __dir__)
 
       class << self
@@ -44,7 +25,7 @@ class << self
 
       def initialize(output, _options = {})
         @directory = self.class.base_directory
-        @todos = Hash.new { |hash, cop_name| hash[cop_name] = Todo.new(cop_name) }
+        @todos = Hash.new { |hash, cop_name| hash[cop_name] = CopTodo.new(cop_name) }
         @todo_dir = TodoDir.new(directory)
         @config_inspect_todo_dir = load_config_inspect_todo_dir
         @config_old_todo_yml = load_config_old_todo_yml
@@ -65,8 +46,8 @@ def file_finished(file, offenses)
 
       def finished(_inspected_files)
         @todos.values.sort_by(&:cop_name).each do |todo|
-          yaml = to_yaml(todo)
-          path = @todo_dir.write(todo.cop_name, yaml)
+          todo.previously_disabled = previously_disabled?(todo)
+          path = @todo_dir.write(todo.cop_name, todo.to_yaml)
 
           output.puts "Written to #{relative_path(path)}\n"
         end
@@ -90,27 +71,6 @@ def relative_path(path)
         path.delete_prefix("#{parent}/")
       end
 
-      def to_yaml(todo)
-        yaml = []
-        yaml << '---'
-        yaml << '# Cop supports --auto-correct.' if todo.autocorrectable?
-        yaml << "#{todo.cop_name}:"
-
-        if previously_disabled?(todo)
-          yaml << "  # Offense count: #{todo.offense_count}"
-          yaml << '  # Temporarily disabled due to too many offenses'
-          yaml << '  Enabled: false'
-        end
-
-        yaml << '  Exclude:'
-
-        files = todo.files.sort.map { |file| "    - '#{file}'" }
-        yaml.concat files
-        yaml << ''
-
-        yaml.join("\n")
-      end
-
       def check_multiple_configurations!
         cop_names = @config_inspect_todo_dir.keys & @config_old_todo_yml.keys
         return if cop_names.empty?
diff --git a/spec/rubocop/cop_todo_spec.rb b/spec/rubocop/cop_todo_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..978df2c01ee95c25289d42cd61381d978d58ebae
--- /dev/null
+++ b/spec/rubocop/cop_todo_spec.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative '../../rubocop/cop_todo'
+
+RSpec.describe RuboCop::CopTodo do
+  let(:cop_name) { 'Cop/Rule' }
+
+  subject(:cop_todo) { described_class.new(cop_name) }
+
+  describe '#initialize' do
+    it 'initializes a cop todo' do
+      expect(cop_todo).to have_attributes(
+        cop_name: cop_name,
+        files: be_empty,
+        offense_count: 0,
+        previously_disabled: false
+      )
+    end
+  end
+
+  describe '#record' do
+    it 'records offenses' do
+      cop_todo.record('a.rb', 1)
+      cop_todo.record('b.rb', 2)
+
+      expect(cop_todo).to have_attributes(
+        files: contain_exactly('a.rb', 'b.rb'),
+        offense_count: 3
+      )
+    end
+  end
+
+  describe '#autocorrectable?' do
+    subject { cop_todo.autocorrectable? }
+
+    context 'when found in rubocop registry' do
+      before do
+        fake_cop = double(:cop, support_autocorrect?: autocorrectable) # rubocop:disable RSpec/VerifiedDoubles
+
+        allow(described_class).to receive(:find_cop_by_name)
+          .with(cop_name).and_return(fake_cop)
+      end
+
+      context 'when autocorrectable' do
+        let(:autocorrectable) { true }
+
+        it { is_expected.to be_truthy }
+      end
+
+      context 'when not autocorrectable' do
+        let(:autocorrectable) { false }
+
+        it { is_expected.to be_falsey }
+      end
+    end
+
+    context 'when not found in rubocop registry' do
+      before do
+        allow(described_class).to receive(:find_cop_by_name)
+          .with(cop_name).and_return(nil).and_call_original
+      end
+
+      it { is_expected.to be_falsey }
+    end
+  end
+
+  describe '#to_yaml' do
+    subject(:yaml) { cop_todo.to_yaml }
+
+    context 'when autocorrectable' do
+      before do
+        allow(cop_todo).to receive(:autocorrectable?).and_return(true)
+      end
+
+      specify do
+        expect(yaml).to eq(<<~YAML)
+          ---
+          # Cop supports --auto-correct.
+          #{cop_name}:
+            Exclude:
+        YAML
+      end
+    end
+
+    context 'when previously disabled' do
+      specify do
+        cop_todo.record('a.rb', 1)
+        cop_todo.record('b.rb', 2)
+        cop_todo.previously_disabled = true
+
+        expect(yaml).to eq(<<~YAML)
+          ---
+          #{cop_name}:
+            # Offense count: 3
+            # Temporarily disabled due to too many offenses
+            Enabled: false
+            Exclude:
+              - 'a.rb'
+              - 'b.rb'
+        YAML
+      end
+    end
+
+    context 'with multiple files' do
+      before do
+        cop_todo.record('a.rb', 0)
+        cop_todo.record('c.rb', 0)
+        cop_todo.record('b.rb', 0)
+      end
+
+      it 'sorts excludes alphabetically' do
+        expect(yaml).to eq(<<~YAML)
+        ---
+        #{cop_name}:
+          Exclude:
+            - 'a.rb'
+            - 'b.rb'
+            - 'c.rb'
+        YAML
+      end
+    end
+  end
+end
diff --git a/spec/rubocop/formatter/todo_formatter_spec.rb b/spec/rubocop/formatter/todo_formatter_spec.rb
index fcff028f07d222cdc14d1f6e30350fb38706ddcd..df56ee4593155230759a517135fdbc95526ec804 100644
--- a/spec/rubocop/formatter/todo_formatter_spec.rb
+++ b/spec/rubocop/formatter/todo_formatter_spec.rb
@@ -261,16 +261,12 @@ def fake_offense(cop_name)
     double(:offense, cop_name: cop_name)
   end
 
-  def stub_rubocop_registry(**cops)
-    rubocop_registry = double(:rubocop_registry)
-
-    allow(RuboCop::Cop::Registry).to receive(:global).and_return(rubocop_registry)
-
-    allow(rubocop_registry).to receive(:find_by_cop_name)
-      .with(String).and_return(nil)
+  def stub_rubocop_registry(cops)
+    allow(RuboCop::CopTodo).to receive(:find_cop_by_name)
+      .with(String).and_return(nil).and_call_original
 
     cops.each do |cop_name, attributes|
-      allow(rubocop_registry).to receive(:find_by_cop_name)
+      allow(RuboCop::CopTodo).to receive(:find_cop_by_name)
         .with(cop_name).and_return(fake_cop(**attributes))
     end
   end