টাফের জন্য Cypress অটোমেটেড টেস্ট

অটোমেটেড টেস্ট গাড়ির ব্রেকের মতো। ব্রেক ছাড়া বেশি গতিতে গাড়ি চালানোর সাহস হয় না।

টাফের ডেভেলপমেন্টের দিক থেকে ২০২১ সাল দুর্দান্তভাবে শুরু হয়েছিল। কমিউনিটির প্রিয় কিছু ফিচার লঞ্চের পাশাপাশি কোডবেস ও ইন্টার্নাল ফ্রেমওয়ার্ক উন্নত করতে প্রচুর কাজ হয়েছে।

এই সময়ে আপডেট ডেপ্লয়ের আগে টাফ ম্যানুয়ালি টেস্ট করা ক্রমশ কঠিন হয়ে পড়ছিল। টাফে ইতোমধ্যে ক্রিটিকাল অংশগুলো কভার করার জন্য ইউনিট টেস্ট ছিল (ভবিষ্যতের একটি ইঞ্জিনিয়ারিং ব্লগ পোস্টে এ নিয়ে আরও লেখা হবে), কিন্তু ভালোভাবে লেখা এন্ড-টু-এন্ড টেস্টের বিকল্প সেগুলো নয়।

Cypress দিয়ে টেস্টিং

Cypress এই কাজের জন্য একদম মানানসই মনে হয়েছিল।

Cypress দিয়ে শুরু করা সহজ ছিল। কয়েকটি টেস্ট লিখে Cypress কীভাবে কাজ করে সেটা বোঝার পর, এটি টাফের কন্টিনিউয়াস ইন্টিগ্রেশন (CI) সার্ভিসের সাথে যুক্ত করা হয়েছে। এখন টাফের নতুন ভার্সন ডেপ্লয়ের আগে পুরো অটোমেটেড টেস্ট স্যুট চলে।

MongoDB সিডিং

টেস্টগুলো ডেটাবেসের একটি পরিচিত অবস্থায় চলে কিনা তা নিশ্চিত করতে mongo-seeding প্যাকেজ ব্যবহার করা হয়েছে। এটি পূর্বনির্ধারিত কালেকশন ও ডকুমেন্ট দিয়ে MongoDB সিড করার সুযোগ দেয়।

fixtures ডিরেক্টরিতে সিড করার ডকুমেন্টগুলো রেখে মাত্র কয়েক লাইন কোডে সিডিং প্রক্রিয়া শুরু করা যায়।

fixtures
└── seed
    └── mongodb
        ├── 1-configurations
        │   └── index.js
        ├── 1-contestFormats
        │   └── index.js
        ├── 1-languages
        │   ├── bash_5.0.js
        │   ├── c++17_gcc9.2.js
        │   └── java_1.8.js
        ├── 1-quotas
        │   └── index.js
        ├── 2-accounts
        │   ├── beifong.js
        │   └── blookz.js
        ├── 2-contests
        │   ├── ended.js
        │   ├── future.js
        │   ├── private.js
        │   └── running.js
        ├── 2-problems
        │   ├── hello-world.js
        │   └── lorem-ipsum.js
        ├── 3-challenges
        │   └── index.js
        ├── 3-participants
        │   └── index.js
        └── 3-submissions
            └── index.js

উদাহরণ হিসেবে, 1-languages/bash_5.0.js দেখতে এরকম:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const { getObjectId } = require('mongo-seeding')

module.exports = {
	_id: getObjectId('language:c++17_gcc9.2'),
	label: 'C++17 GCC 9.2',
	family: 'C++',
	label_lower: 'c++17 gcc 9.2',
	stack: 'g++9',
	flags: {},
	exts: ['.cpp', '.cc'],
	needs_build: true,
	needs_filename: false,
	syntax: 'cpp',
	code_mirror_mode: 'text/x-c++src',
	initial_template: '#include <iostream>\n\nusing namespace std;\n\nint main() {\n	\n	return 0;\n}\n',
	initial_filename: '',
	scanspec_generator: 'cpp14',
	language_server: 'clangd11/cpp',
	enabled: true,
	notes: '',
	notes_html: '',
	suite: 'GCC 9.2.0',
	hello_toph: '#include <iostream>\n\nusing namespace std;\n\nint main() {\n	cout << "Hello Toph!" << endl;\n	return 0;\n}',
	command: 'g++ -static -s -x c++ -O2 -std=c++17 -D ONLINE_JUDGE hello.cpp -lm\n./a.out\n',
	website: 'https://gcc.gnu.org/',
	deprecated: false,
	experimental: false,
	created_at: new Date(),
	modified_at: new Date()
}

লক্ষ্য করুন, ডকুমেন্টটি একটি সাধারণ JavaScript অবজেক্ট হিসেবে সংজ্ঞায়িত করা হয়েছে।

getObjectId ফাংশন অনুমানযোগ্য MongoDB ObjectID তৈরি করা সহজ করে। একই স্ট্রিং দিলে এটি সবসময় একই ObjectID দেয়।

নিচের কোড কয়েকটি লাইন একটি Cypress টাস্কে মুড়িয়ে দেওয়া হয়েছে যাতে টেস্ট কোডের যেকোনো জায়গা থেকে এটি ট্রিগার করা যায়।

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const seeder = new Seeder({
	database: {
		host: process.env.MONGO_HOST || 'localhost',
		port: 27017,
		name: 'toph-arena',
	},
	dropDatabase: true,
	mongoClientOptions: {
		w: 'majority'
	}
})
const collections = seeder.readCollectionsFromPath(path.resolve('cypress/fixtures/seed/mongodb'))
seeder.import(collections)

টেস্ট লেখা

Cypress যেটা সবচেয়ে ভালো করে তা হলো — স্বাভাবিকভাবে অ্যাসিঙ্ক্রোনাস একটি ভাষায় সিঙ্ক্রোনাস প্রোগ্রামিংয়ের অনুভূতি দেয়। এতে টেস্ট কোড পড়তে ও বুঝতে সহজ হয়।

নিচের উদাহরণে দেখুন ইন্টিগ্রেটেড কোড এডিটর কীভাবে টেস্ট করা হয়:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
describe('CodePanel', () => {
	beforeEach(() => {
		cy.task('db:seed')
		faker.seed(0)

		cy.clearLocalStorage()

		cy.login('blookz', '[REDACTED]')
	})

	context('Problem Archive', () => {
		beforeEach(() => {
			cy.visit('/p/lorem-ipsum')
			cy.waitForResources(/codepanel(\.[0-9a-f]+)?\.js/)
			cy.get('.btn-codepanel').click()

			cy.get('.codepanel').should('have.class', 'open')
		})

		// ...

		it('can submit code', () => {
			cy.get('.codepanel select[name="languageId"]').select('Bash 5.0').wait(25)
			const source = faker.lorem.word()
			cy.get('.codepanel .cm-editor .cm-content').focus().type(`echo "${source}"{enter}`, {delay: 25})
			cy.get('.codepanel .btn-submit').click()

			cy.get('#mdlSubmissionContent').should('be.visible')
			cy.get('#mdlSubmissionContent h4').should('contain', 'Submission')
		})
	})
})

ইন্টিগ্রেটেড কোড এডিটরের প্রতিটি টেস্টের আগে ডেটাবেস সিড করা হয়, র‍্যান্ডম ডেটা জেনারেটরকে একটি পরিচিত সিডে সেট করা হয়, ব্রাউজারের localStorage পরিষ্কার করা হয়, এবং একটি ইউজার সেশন শুরু করা হয় (অর্থাৎ লগইন করা হয়)।

এই টেস্টের লক্ষ্য হলো যাচাই করা যে ব্যবহারকারী আর্কাইভ থেকে কোনো সমস্যা সমাধান করার সময় ইন্টিগ্রেটেড কোড এডিটরের মাধ্যমে কোড সাবমিট করতে পারে কিনা।

টেস্ট কোড Cypress-কে নির্দেশ দেয়: একটি সমস্যার পেজে যেতে, ইন্টিগ্রেটেড কোড এডিটর লোড হওয়ার জন্য অপেক্ষা করতে, এডিটর খোলার বাটনে ক্লিক করতে, এডিটরে কিছু টেক্সট লিখতে, এবং সাবমিট বাটনে ক্লিক করতে।

এই টেস্টে প্রত্যাশিত আচরণ হলো কোড সাবমিট হলে একটি মডাল খুলবে।

নেটওয়ার্ক রিকোয়েস্ট

কিছু টেস্টে নেটওয়ার্ক রিকোয়েস্ট সরাসরি অটোমেটেড টেস্টের জন্য চালানো ব্যাকএন্ড ইন্সট্যান্সে পাঠানো যুক্তিসংগত। অন্য ক্ষেত্রে সেই নেটওয়ার্ক রিকোয়েস্ট ইন্টারসেপ্ট করে প্রতিক্রিয়া জানানো বেশি কার্যকর।

যেমন, ইন্টিগ্রেটেড এডিটর এমন প্রোগ্রামিং ভাষাগুলো সঠিকভাবে হ্যান্ডেল করে কিনা যেখানে ব্যবহারকারী সাবমিশনের ফাইলনাম পরিবর্তন করতে পারে — এটি পরীক্ষা করতে শুধু যাচাই করতে হয় API কলে সঠিক কন্টেন্ট আছে কিনা। নিচের কোড ঠিক এটাই টেস্ট করে:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
it('can submit code with filename', () => {
	cy.intercept('POST', /\/api\/(problems|challenges)\/[0-9a-f]{24}\/submissions/, req => {
		req.reply(400, 'ErrThrottled')
	}).as('postSubmission')

	cy.get('.codepanel select[name="languageId"]').select('Java 1.8').wait(25)
	const filename = faker.lorem.word() + '.java'
	cy.get('.codepanel .cm-editor .cm-content').focus().type('{selectall}class Hello {\n  static public void main(String args[]) {\n    System.out.println("Hello Toph!");\n  }\n}', {delay: 25})
	cy.get('.codepanel input[name=filename]').should('be.visible').clear().type(filename)
	cy.get('.codepanel .btn-submit').click()

	cy.wait('@postSubmission').its('request.body').should('include', `filename="${filename}"`)
})

টেস্টের প্রথম তিন লাইনে “create submission” এন্ডপয়েন্টে করা API কল ইন্টারসেপ্ট করে একটি পূর্বনির্ধারিত রেসপন্স দিয়ে স্টাব করা হচ্ছে। টেস্টের একদম শেষ লাইনে যাচাই করা হচ্ছে API কল আদৌ হয়েছে কিনা এবং হলে রিকোয়েস্ট বডিতে ইন্টিগ্রেটেড কোড এডিটরে দেওয়া ফাইলনামটি আছে কিনা।

GitLab CI/CD

Cypress টেস্টকে GitLab CI-এর অংশ করা মোটামুটি সহজ ছিল:

  • Cypress-এর অফিসিয়াল Docker ইমেজের উপর ভিত্তি করে একটি Docker ইমেজ তৈরি করা হয়েছে, যেখানে tophd (ওয়েবসার্ভার) প্রসেস চালু করার জন্য প্রয়োজনীয় ডিপেন্ডেন্সি যোগ করা হয়েছে।
  • টাফের বিদ্যমান GitLab CI কনফিগারেশনের টেস্ট স্টেজে একটি টেস্ট জব যোগ করা হয়েছে।
    • ডেপ্লয়ের আগে এবং পাইপলাইন ম্যানুয়ালি ট্রিগার করলে Cypress টেস্ট চলে।
    • Cypress-এর নেওয়া স্ক্রিনশট ও রেকর্ড করা ভিডিও আর্টিফ্যাক্ট হিসেবে সংরক্ষিত থাকে, যাতে সমস্যা ডিবাগ করা সহজ হয়।
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
test cypress:
  image:
    name: # REDACTED
  stage: test
  only:
    - tags
    - web
  services:
    - redis
    - mongo
    - rabbitmq
  script:
    - echo "$CYPRESS_ENV" > .env
    - ./tophd &
    - MONGO_HOST=mongo make cypress SKIP_DOCKER=true
  needs:
    - build binaries
    - build assets
  artifacts:
    when: always
    expire_in: 1 day
    paths:
      - cypress/screenshots
      - cypress/videos
  cache:
    key: "assets"
    paths:
      - .npm
      - .cypress-cache
      - node_modules
    policy: pull
  interruptible: true

পরবর্তী পদক্ষেপ

স্বল্পমেয়াদী লক্ষ্যগুলোর মধ্যে একটি হলো সবচেয়ে গুরুত্বপূর্ণ ব্যবহারকারীর ইন্টারঅ্যাকশনগুলো কভার করতে টেস্ট লেখা। এর মধ্যে রয়েছে টাফে ব্যবহারকারীর নিবন্ধন থেকে শুরু করে আর্কাইভ থেকে সমস্যা সমাধান এবং টাফে কন্টেস্ট আয়োজন বা অংশগ্রহণ পর্যন্ত সব কিছু।

টেস্টের এই আরও ব্যাপক স্যুট ভবিষ্যতে টাফের ডেভেলপমেন্ট উল্লেখযোগ্যভাবে ত্বরান্বিত করবে।