Pair programming with Github Copilot Chat

Published
2023-07-22
Tags

At 2023.7.21, Github Copilot Chat beta now available for every organization!!


VSCode extensions are also available.


So, I tried pair programming with Github copilot chat in some specific examples.


Example: Refactoring GraphQL queries by every resolver

The following code is a GraphQL query implementation to get the data for a simple blog application by using graphql-ruby.


ex. query_type.rb

module Types
  class QueryType < Types::BaseObject
    include GraphQL::Types::Relay::HasNodeField
    include GraphQL::Types::Relay::HasNodesField

    field :user, Types::UserType, null: true do
      description "Find User by ID"
      argument :id, ID, required: true
    end
    def user(id:)
      User.find(id)
    end

    field :users, [Types::UserType], null: false do
      description "Fetch All Users"
    end
    def users(page: nil, items: nil)
      User.all
    end

    field :article, Types::ArticleType, null: true do
      argument :id, ID, required: true
    end
    def article(id:)
      Article.find(id)
    end

    field :articles, [Types::ArticleType], null: true do
      argument :user_id, ID, required: false
    end
    def articles(user_id: nil)
      if user_id
        Article.where(user_id: user_id)
      else
        Article.all
      end
    end

    field :comments, [Types::CommentType], null: false do
      argument :id, ID, required: false
      argument :user_id, ID, required: false
      argument :article_id, ID, required: false
    end
    def comments(id: nil, user_id: nil, article_id: nil)
      scope = Comment.all
      scope = scope.where(id: id) if id
      scope = scope.where(user_id: user_id) if user_id
      scope = scope.where(article_id: article_id) if article_id
      scope
    end

    field :tags, [Types::TagType], null: false do
      argument :name, String, required: false
    end
    def tags(name: nil)
      if name
        Tag.where("name like ?", "#{name}%")
      else
        Tag.all
      end
    end
  end
end


The problem with this implementation is that query_type.rb contains all the query implementation.

So, I will try to refactor it using Github copilot chat to separate the query implementation for each Resolver class.

First prompt: “Refactor the selected code”

First, I selected all implementations of query_type.rb.

And then, I asked Github Copilot the way.

Oops…, this prompt was too abstract.

Second prompt: “Refactoring the GraphQL query implemented in graphql-ruby to separate each Resolver”


Github Copilot:

“Sure, here's a possible refactored version of the code that separates each resolver into its own method:”
module Types
  class QueryType < Types::BaseObject
    include GraphQL::Types::Relay::HasNodeField
    include GraphQL::Types::Relay::HasNodesField

    field :user, Types::UserType, null: true do
      description "Find User by ID"
      argument :id, ID, required: true
    end
    def user(id:)
      User.find(id)
    end

    field :users, [Types::UserType], null: false do
      description "Fetch All Users"
    end
    def users
      User.all
    end

    field :article, Types::ArticleType, null: true do
      argument :id, ID, required: true
    end
    def article(id:)
      Article.find(id)
    end

    field :articles, [Types::ArticleType], null: true do
      argument :user_id, ID, required: false
    end
    def articles(user_id: nil)
      if user_id
        Article.where(user_id: user_id)
      else
        Article.all
      end
    end

    field :comments, [Types::CommentType], null: false do
      argument :id, ID, required: false
      argument :user_id, ID, required: false
      argument :article_id, ID, required: false
    end
    def comments(id: nil, user_id: nil, article_id: nil)
      scope = Comment.all
      scope = scope.where(id: id) if id
      scope = scope.where(user_id: user_id) if user_id
      scope = scope.where(article_id: article_id) if article_id
      scope
    end

    field :tags, [Types::TagType], null: false do
      argument :name, String, required: false
    end
    def tags(name: nil)
      if name
        Tag.where("name like ?", "#{name}%")
      else
        Tag.all
      end
    end
  end
end

Hmm….., It is the same as the previous implementation.😓


Third prompt: “No! Now, query_type.rb includes all queries, but I want to divide resolver class.”

Github Copilot:

I apologize for misunderstanding your previous request. To separate each resolver into its own class, you can create a new file for each resolver and define a class that inherits from GraphQL::Schema::Resolver. Here's an example of how you could refactor the code:
Create a new file called user_resolver.rb and define a class called UserResolver:
class UserResolver < GraphQL::Schema::Resolver
  description "Find User by ID"

  argument :id, ID, required: true

  type Types::UserType, null: true

  def resolve(id:)
    User.find(id)
  end
end
Create a new file called users_resolver.rb and define a class called UsersResolver:
class UsersResolver < GraphQL::Schema::Resolver
  description "Fetch All Users"

  type [Types::UserType], null: false

  def resolve
    User.all
  end
end
Create a new file called article_resolver.rb and define a class called ArticleResolver:
class ArticleResolver < GraphQL::Schema::Resolver
  argument :id, ID, required: true

  type Types::ArticleType, null: true

  def resolve(id:)
    Article.find(id)
  end
end

And the rest of the query output the same pattern of implementation.

Finally, in your QueryType class, replace each field with a call to the corresponding resolver class:
module Types
  class QueryType < Types::BaseObject
    include GraphQL::Types::Relay::HasNodeField
    include GraphQL::Types::Relay::HasNodesField

    field :user, resolver: UserResolver
    field :users, resolver: UsersResolver
    field :article, resolver: ArticleResolver
    field :articles, resolver: ArticlesResolver
    field :comments, resolver: CommentsResolver
    field :tags, resolver: TagsResolver
  end
end

Sounds good!

Github copilot presented an implementation that separates each Resolver by class.

Forth prompt: “What directory structure should the resolver class have?”

Github Copilot:

The directory structure for resolver classes is flexible and can be organized in a way that makes sense for your application. However, a common convention is to create a resolvers directory under the app/graphql directory, and then create a separate file for each resolver class within the resolvers directory. Here's an example directory structure:
app/graphql/
├── resolvers/
│   ├── article_resolver.rb
│   ├── articles_resolver.rb
│   ├── comment_resolver.rb
│   ├── comments_resolver.rb
│   ├── tag_resolver.rb
│   └── tags_resolver.rb
├── types/
│   ├── article_type.rb
│   ├── comment_type.rb
│   ├── mutation_type.rb
│   ├── query_type.rb
│   └── tag_type.rb
└── schema.rb
In this example, each resolver class is defined in its own file within the resolvers directory. The types directory contains the definition of each GraphQL type used in the schema, and schema.rb contains the definition of the GraphQL schema itself.


Almost perfect!!

It seems to me that we could adopt the improvements suggested by Github Copilot.


Github Copilot Chat makes it easy to add implementations.

Like this👇


Final prompt: “Output a Request spec with RSpec using each Resolver.”


Github Copilot:

Sure, here's an example of how you could write a request spec using RSpec for each resolver:
require 'rails_helper'

RSpec.describe "GraphQL queries", type: :request do
  describe "user query" do
    let(:user) { create(:user) }
    let(:query) do
      <<~GQL
        query {
          user(id: #{user.id}) {
            id
            name
            email
          }
        }
      GQL
    end

    it "returns the user with the specified ID" do
      post '/graphql', params: { query: query }
      json = JSON.parse(response.body)
      data = json['data']['user']
      expect(data).to include(
        'id' => user.id.to_s,
        'name' => user.name,
        'email' => user.email
      )
    end
  end

  describe "users query" do
    let!(:users) { create_list(:user, 3) }
    let(:query) do
      <<~GQL
        query {
          users {
            id
            name
            email
          }
        }
      GQL
    end

    it "returns all users" do
      post '/graphql', params: { query: query }
      json = JSON.parse(response.body)
      data = json['data']['users']
      expect(data.size).to eq(users.size)
      data.each_with_index do |user_data, i|
        expect(user_data).to include(
          'id' => users[i].id.to_s,
          'name' => users[i].name,
          'email' => users[i].email
        )
      end
    end
  end

  describe "article query" do
    let(:article) { create(:article) }
    let(:query) do
      <<~GQL
        query {
          article(id: #{article.id}) {
            id
            title
            body
          }
        }
      GQL
    end

    it "returns the article with the specified ID" do
      post '/graphql', params: { query: query }
      json = JSON.parse(response.body)
      data = json['data']['article']
      expect(data).to include(
        'id' => article.id.to_s,
        'title' => article.title,
        'body' => article.body
      )
    end
  end

  describe "articles query" do
    let!(:articles) { create_list(:article, 3) }
    let(:query) do
      <<~GQL
        query {
          articles {
            id
            title
            body
          }
        }
      GQL
    end

    it "returns all articles" do
      post '/graphql', params: { query: query }
      json = JSON.parse(response.body)
      data = json['data']['articles']
      expect(data.size).to eq(articles.size)
      data.each_with_index do |article_data, i|
        expect(article_data).to include(
          'id' => articles[i].id.to_s,
          'title' => articles[i].title,
          'body' => articles[i].body
        )
      end
    end

    it "returns articles for a specific user" do
      user = create(:user)
      articles_for_user = create_list(:article, 2, user: user)
      post '/graphql', params: { query: query, variables: { userId: user.id } }
      json = JSON.parse(response.body)
      data = json['data']['articles']
      expect(data.size).to eq(articles_for_user.size)
      data.each_with_index do |article_data, i|
        expect(article_data).to include(
          'id' => articles_for_user[i].id.to_s,
          'title' => articles_for_user[i].title,
          'body' => articles_for_user[i].body
        )
      end
    end
  end

  describe "comments query" do
    let!(:comments) { create_list(:comment, 3) }
    let(:query) do
      <<~GQL
        query {
          comments {
            id
            body
          }
        }
      GQL
    end

    it "returns all comments" do
      post '/graphql', params: { query: query }
      json = JSON.parse(response.body)
      data = json['data']['comments']
      expect(data.size).to eq(comments.size)
      data.each_with_index do |comment_data, i|
        expect(comment_data).to include(
          'id' => comments[i].id.to_s,
          'body' => comments[i].body
        )
      end
    end

    it "returns comments for a specific user" do
      user = create(:user)
      comments_for_user = create_list(:comment, 2, user: user)
      post '/graphql', params: { query: query, variables: { userId: user.id } }
      json = JSON.parse(response.body)
      data = json['data']['comments']
      expect(data.size).to eq(comments_for_user.size)
      data.each_with_index do |comment_data, i|
        expect(comment_data).to include(
          'id' => comments_for_user[i].id.to_s,
          'body' => comments_for_user[i].body
        )
      end
    end

    it "returns comments for a specific article" do
      article = create(:article)
      comments_for_article = create_list(:comment, 2, article: article)
      post '/graphql', params: { query: query, variables: { articleId: article.id } }
      json = JSON.parse(response.body)
      data = json['data']['comments']
      expect(data.size).to eq(comments_for_article.size)
      data.each_with_index do |comment_data, i|
        expect(comment_data).to include(
          'id' => comments_for_article[i].id.to_s,
          'body' => comments_for_article[i].body
        )
      end
    end
  end

  describe "tags query" do
    let!(:tags) { create_list(:tag, 3) }
    let(:query) do
      <<~GQL
        query {
          tags {
            id
            name
          }
        }
      GQL
    end

    it "returns all tags" do
      post '/graphql', params: { query: query }
      json = JSON.parse(response.body)
      data = json['data']['tags']
      expect(data.size).to eq(tags.size)
      data.each_with_index do |tag_data, i|
        expect(tag_data).to include(
          'id' => tags[i].id.to_s,
          'name' => tags[i].name
        )
      end
    end

    it "returns tags with a specific name" do
      tag = create(:tag, name: "ruby")
      post '/graphql', params: { query: query, variables: { name: "ru" } }
      json = JSON.parse(response.body)
      data = json['data']['tags']
      expect(data.size).to eq(1)
      expect(data[0]).to include(
        'id' => tag.id.to_s,
        'name' => tag.name
      )
    end
  end
end

This is amazing.😲

This test seems appropriate!

Summary

Features I've found using Github Copilot Chat

  • Github Copilot Chat gives higher quality answers than I expected
  • Response time to output answers is also fast
  • Easy to add implementations to the editor on VSCode
  • It suggests potential next questions