杜欣翰
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.

      Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Explore these features while you wait
      Complete general settings
      Bookmark and like published notes
      Write a few more notes
      Complete general settings
      Write a few more notes
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    ###### tags: `單元測試` # 關於單元測試 ## 開始之前先推薦一本書 Kent Beck的測試驅動開發:案例導向的逐步解決之道 https://www.books.com.tw/products/0010883019 ## 🤔為甚麼要寫測試 ### 測試即文件 > 「所有專案都是由兩個人以上所開發的」 一個是你,另一個是別人 (或是N個月後的你)。 當一個不清楚細節的人進到專案裡面時,他可以透過閱讀測試案例來了解該程式碼背後的用意。 ### 提高信心/降低修改程式碼的恐懼 大規模重構的時候可以放膽去做,改完一跑測試馬上知道有沒有通過。 ### 程式碼品質 - 單一責任原則 - 模組化 - 思考程式碼真正要做的功能 <!-- 當編寫組件時要牢記測試,最終會創建孤立的,更可重用的組件。如果您開始為組件編寫測試,但發現它們不容易測試,則表明您可以重構組件,這最終可以改善它們。 --> --- ## 👀測試的方法論 <!-- 方法論不是這篇文章的重點,所以就用一張圖解釋。 --> ![](https://i.imgur.com/vaLocp6.png) ## 🔺測試金字塔 ### 甚麼是測試金字塔? ![](https://martinfowler.com/articles/practical-test-pyramid/testPyramid.png) > **測試金字塔**。邁克•科恩(Mike Cohn)在他的著作 “*Succeeding with Agile*” 中提出了這個概念 (2009 年)。 測試金字塔表示測試的組成應該由三層組成 (由下到上) 1. UI測試 (E2E 測試) 2. 整合測試 3. 單元測試 ### 兩個重要概念 1. 撰寫不同粒度的測試 2. 測試在越高的層次, 所要進行的測試比例要越少 而撰寫的數量應該要是 單元測試 > 整合測試 > E2E 測試 而不是像一個**冰淇淋甜筒** ![](https://miro.medium.com/max/1000/0*3WBRay7P9EyXP3ms.png) 應該要比較像這樣 ![](https://alisterbscott.com/wp-content/uploads/2018/02/ideal-automated-testing-pyramid.jpg) > ### 不需要太執著於名稱 > 不要對科恩測試金字塔中各個圖層的名稱過於重視。它們可能會引起誤解 > 最重要的是找到適合自己和團隊的術語,明確的編寫不同類型的測試 > **在現在前端 SPA 框架盛行的時代,UI測試也可以是單元測試** --- ## 🙋甚麼是單元? (提問) <!-- 如果你問三個人,可能會收到四種答案,在某種程度上是沒有正確規範的。 以前端的角度來說,「Function」、「Class method」、「Module」或「Component」都是可測試的標的 --> ## F.I.R.S.T 原則 #### Fast 測試如果跑得不夠快,就不會讓人想常常跑,不常跑的測試最後也就失去的它意義了。 所以實務上會使用mock工具,mock其他依賴的物件或環境,來加速測試的執行。 #### Independent 測試要相互獨立,一個測試不會依賴其他測試,如果互相依賴的話,一個測試的失敗會影響其他測試也跟著失敗,那麼在找問題點的時候將會變得更困難。 #### Repeatable 測試應該要可以在任何環境中重複執行。減少因環境因素而產生測試失敗的問題。 #### Self-validating 測試應該要輸出 Boolean,讓人能夠分辨哪些測試通過與沒通過。作者在書中提到的這點,現今有許多 IDE 都已經能夠輔助做到這一點,例如 vscode 便能夠顯示通過與沒通過的單元測試。 #### Thorough 徹底 測試不應該只追求涵蓋率 100%,更應該追求測盡所有的使用情境。包括 boundary value、數量龐大的資料集、資安、大數、例外處理與非預期的函數數量或輸入。 ## ⚠️測試要(不要)做甚麼? #### 不要測試框架 Vue.js、vue-router、Vuex,這些東西通常作者都已經寫過測試了,我們不需要去測試它。 #### 不要測試第三方庫 如果你使用的第三方 library 並沒有良好的測試的話可能就得考慮該不該使用它,一旦用了你就得相信它 #### 不要過於關注 function 的細節,應該要測試的是可以觀察到的行為。 原因是因為重構的時候有可能會改變內部的實作細節,那麼你的單元測試就壞掉了,然後你就會對那些愚蠢的測試失敗感到厭煩。 ![](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1579559461870_1.opt.png?alt=media&token=714488d6-8d50-4f4a-8f6f-597f83a90854) #### 不盲目的追求測試覆蓋率 這一點其實跟上一點是有相關的,因為我們應該更在意的是測試輸入與輸出,而不是細節的代碼,所以覆蓋率可能不會太高,覆蓋率越高也不代表就是完美的,**測試是重質不重量**。 ## 🏁單元測試的步驟: 3A * Arrange: 設置測試資料 * Act: 調用您的被測方法 * Assert: 斷言預期返回的結果 --- ## ⚒️工具 要在 Vue.js 中進行測試的話需要安裝 `@vue/test-utils` 和 `jest`, ### [vue-test-utils](https://vue-test-utils.vuejs.org/) Vue.js 的官方單元測試函式庫。提供許多 API 給開發者進行測試。 例如: monut()、shallowMount()、setProps、destroy... ### [Jest](https://jestjs.io/en/) Jest是一個令人愉悅的JavaScript測試框架,專注於簡單性。 它適用於使用以下項目的項目:Babel,TypeScript,Node,React,Angular,Vue等! **他號稱...** - zero config - snaoshots - isolated - great api - etc. --- # 實作 ## 安裝 請參考下面兩個連結 https://vue-test-utils.vuejs.org/zh/guides/getting-started.html#%E5%AE%89%E8%A3%85 https://vue-test-utils.vuejs.org/zh/guides/#%E7%94%A8-jest-%E6%B5%8B%E8%AF%95%E5%8D%95%E6%96%87%E4%BB%B6%E7%BB%84%E4%BB%B6 裝完之後就會一個 npm script `test:unit`,但這個是下一次跑一次的指令 所以我們可以多一行 `"test:unit:watch": "vue-cli-service test:unit --watch"` 這樣就可以持續跑著 jest,修改完儲存馬上再跑一次。 ## 測試檔案擺放位置 Jest 推薦你在被測試代碼的所在目錄下創建一個 `__tests__` 目錄 ``` 📂src ┣ 📂components ┃ ┗ 📜Button.vue ┃ ┗ 📂__tests__ ┃ ┗ 📜Button.spec.js // xxx.spec.js or xxx.test.js 皆允許 ┣ 📂views ┃ ┗ 📜login.vue ┃ ┗ 📂__tests__ ┃ ┗ 📜login.test.js ┗ 📂other ``` --- ## Jest API ### describe describe 創造一整個區塊,裡面包含許多測試用例 (test case),表示一組相關的測試。 ```javascript= describe("測試 foo 函式", () => { //... }); ``` ### test/it test/it 區塊稱為測試用例,表示一個單獨的測試,是測試中的最小單位。 ```javascript= describe("測試 foo 函式", () => { it("輸入 a, b,應該返回 c", () => { //... }); }); ``` ### expect 斷言 所謂斷言,就是判斷源碼的實際執行結果與預期結果是否一致,如果不一致就拋出一個錯誤。 ```javascript= describe("測試 foo 函式", () => { it("輸入 a, b,應該返回 c", () => { expect(foo(a, b)).toBe(c); }); }); ``` expect() 後面還有許多方法可用,常用的有 - .toBe() - .toEqual() - .toMatch() - .not() - .toBeTruthy(), .toBeFalsy() - .toBeCalled() - ... 更多請參考 [Expect](https://jestjs.io/docs/en/expect) ### beforeEach、afterEach 在每一個測試案例之前/之後,執行某一段程式。 常用來先執行某些重複的行為,例如: 初始化待測物件、銷毀待測物件 ### beforeAll、afterAll 在全部測試案例之前/之後,執行某一段程式。 --- ## Vue test utils API ### mount 回傳一個被掛載和渲染的組件的 `wrapper` ### shallowMount 和 `mount` 一樣,回傳一個被掛載和渲染的組件的 `wrapper`,不同的是 `shallowMount` 會忽略子組件,只掛載組件本身。 <!-- 單元測試可以粗略地分成社交型與孤立型,所謂社交型是 --> 如果你想單獨測試組件的話用 shallowMount,想測試整組的話用 mount ### Wrapper Wrapper 是一個包含掛載組件或是 vnode,也包含了測試他們(被掛載組件或是 vnode)的方法 使用 mount or shallowMount 後可以產生 wrapper 而 wrapper 常用的方法有 ### vm Vue 的實例,wrapper.vm 可以訪問一個實例中的所有方法和屬性 ### .findComponent(), .findAllComponents() 在 wrapper 中尋找要測試的組件 ### .setProps(), .setData() 設置 props、data ### .exists(), .contains() ### .destory() ### .emitted() 更多請參考 [Wrapper](https://vue-test-utils.vuejs.org/api/wrapper/) --- ## 先來個簡單的例子 測試一個加總的函式 <!-- 先建立兩支檔案 `sum.ts`、`sum.spec.ts` --> ```javascript= // sum.ts export function sum(a: number, b: number): number { return a + b; } ``` ```javascript= // sum.spec.ts import { sum } from "@/functions/sum.ts"; describe("測試加總函式", () => { it("1 + 2 應該要等於 3", () => { expect(sum(1, 2)).toBe(3); }); }); ``` <!-- ### 再一個例子 去重 --> ## UI 組件測試 ### 測試渲染 給定 props 「a」 產生 「b」 結果 EX: **測試給定 props 「loggedIn」 後產生的畫面** ```html= <template> <div> <button ref="logOutButton" v-if="loggedIn">登出</button> <button ref="logInButton" v-else>登入</button> </div> </template> <script> export default { props: { loggedIn: Boolean } }; </script> ``` ```javascript= import Vue from 'vue'; import { shallowMount } from '@vue/test-utils'; import Header from '@/components/Header.vue'; describe('測試 Header.vue', () => { it('當 props loggedIn 為 true 時,顯示登出按鈕,反之則顯示登入按鈕', async () => { const wrapper = shallowMount(Header, { propsData: { loggedIn: true }, }); await Vue.nextTick(); expect(wrapper.findComponent({ ref: 'logOutButton' }).exists()).toBe(true); wrapper.setProps({ loggedIn: false }); await Vue.nextTick(); expect(wrapper.findComponent({ ref: 'logInButton' }).exists()).toBe(true); }); }); ``` <!-- ### 測試非同步行為 --> **注意**上面的程式碼有用到 <!-- 在編寫測試程式時你將會遇到兩種非同步行為: 1. 來自 Vue 的更新 2. 來自外部行為的更新 --> ```javascript= await Vue.nextTick() ``` 是因為 setProps 之後,為了斷言這個變化,測試需要等待Vue 完成更新,有兩種辦法,一種就是使用 await Vue.nextTick(),一個更簡單且明確的方式則是 await 那個你變更狀態的方法,例如下面這樣直接 await 非同步行為 ```javascript= await wrapper.setProps({ loggedIn: false }); expect(wrapper.findComponent({ ref: "logInButton" }).exists()).toBe(true); ``` 需要被 await 的方法有: - setData - setValue - setChecked - setSelected - setProps - trigger ### 測試操作 **測試 onclick 後觸發 emit 事件** ```html= <template> <div> <button ref="logOutButton" v-if="loggedIn" @click="$emit('logout')"> 登出 </button> <button ref="logInButton" v-else @click="$emit('login')">登入</button> </div> </template> ``` ```javascript= import Vue from "vue"; import { shallowMount } from "@vue/test-utils"; import Header from "@/components/Header.vue"; describe("測試 Header.vue", () => { //... it("點擊登出按鈕 emit logout 事件", () => { const wrapper = shallowMount(Header, { propsData: { loggedIn: true }, }); const target = wrapper.findComponent({ ref: "logOutButton" }); target.trigger("click"); expect(wrapper.emitted("logout")); }); it("點擊登出按鈕 emit login 事件", () => { const wrapper = shallowMount(Header, { propsData: { loggedIn: false }, }); const target = wrapper.findComponent({ ref: "logInButton" }); target.trigger("click"); expect(wrapper.emitted("login")); }); }); ``` 看到這裡有沒有覺得可以簡化一下了 優化後的版本 ```javascript= //... const factory = (values = {}) => { return shallowMount(Header, { propsData: { ...values } }); }; describe("測試 Header.vue", () => { // ... it("點擊登出按鈕 emit logout 事件", () => { const wrapper = factory({ loggedIn: true }); wrapper.findComponent({ ref: "logOutButton" }).trigger("click"); expect(wrapper.emitted("logout")); }); it("點擊登出按鈕 emit login 事件", () => { const wrapper = factory({ loggedIn: false }); wrapper.findComponent({ ref: "logInButton" }).trigger("click"); expect(wrapper.emitted("login")); }); }); ``` ### 不純 UI 組件測試 (非同步操作) <!-- #### computed --> ### 模擬使用者輸入 ```javascript= <template> <div> <form @submit.prevent="handleSubmitAsync"> <input v-model="username" data-username> <input type="submit"> </form> <div class="message" v-if="submitted" > Thank you for your submission, {{ username }}. </div> </div> </template> <script> export default { name: "FormSubmitter", data() { return { username: "", submitted: false }; }, methods: { handleSubmit() { this.submitted = true; }, handleSubmitAsync() { return this.$http .get("/api/v1/register", { username: this.username }) .then(() => { this.submitted = true; }) .catch(e => { throw Error("Something went wrong", e); }); } } }; </script> ``` ```javascript= import { shallowMount } from "@vue/test-utils"; import FormSubmitter from "@/components/FormSubmitter.vue"; import flushPromises from "flush-promises"; let url = ""; let data = ""; const mockHttp = { get: (_url, _data) => { return new Promise((resolve, reject) => { url = _url; data = _data; resolve(); }); } }; describe("FormSubmitter", () => { it("reveals a notification when submitted", async () => { const wrapper = shallowMount(FormSubmitter, { mocks: { $http: mockHttp } }); wrapper.find("[data-username]").setValue("alice"); wrapper.find("form").trigger("submit.prevent"); await flushPromises(); expect(wrapper.find(".message").text()).toBe( "Thank you for your submission, alice." ); expect(url).toBe("/api/v1/register"); expect(data).toEqual({ username: "alice" }); }); }); ``` ### Vuex todo --- # 總結 對於一個從來沒寫過測試的我來說,寫測試真的是很難開始,而且網路上的測試教學文章不多,還好最後有找到一個還不錯的 [Vue测试指南](https://lmiller1990.github.io/vue-testing-handbook/zh-CN/) ## 何時寫測試 在 Vue 的文件裡面有提到當一個應用開始建立起來並且有真實的用戶對這個應用產生興趣,那麼單元測試就是必要的了。 而我會想要寫測試會是因為我覺得這一段 code 我沒有那麼有把握,因此我寫測試來降低我對於 bug 出現的恐懼。 ## 寫測試的感覺 我覺得比較像是在做程式碼的保險措施,當程式碼越過那條你所設下的界線的時候他就會響起警報,還蠻有趣的。 --- # 參考 ## 🔗參考資料 - https://martinfowler.com/articles/practical-test-pyramid.html#TestStructure - https://watirmelon.blog/testing-pyramids/ - http://otischou.tw/2019/08/02/unit-test.html - https://dev.to/briwa/a-series-of-my-unfortunate-mistakes-when-writing-tests-h8m?utm_source=additional_box&utm_medium=internal&utm_campaign=regular&booster_org=&fbclid=IwAR3H9GfX7CRSka0VzTOBTVjwQhAaArk99ARl-6BjimahFw9XST_rgaiR1Ek - https://medium.com/@envive.tw/%E5%89%8D%E8%A8%80-%E6%9A%B4%E8%B5%B0gandhi-%E4%B8%8D%E7%9F%A5%E9%81%93%E5%A4%A7%E5%AE%B6%E6%9C%89%E6%B2%92%E6%9C%89%E7%8E%A9%E9%81%8E%E4%B8%80%E6%AC%BE%E9%81%8A%E6%88%B2%E5%8F%AB%E5%81%9A-civilization-%E6%96%87%E6%98%8E%E5%B8%9D%E5%9C%8B-441891b116d7 - https://medium.com/@envive.tw/%E5%BE%9E%E7%AF%84%E4%BE%8B%E5%AD%B8%E7%BF%92-vue-js-%E7%9A%84-unit-test-44e6f9f1b903 - [使用 Jest 進行 Front-End Unit Test(線上 React 讀書會版)](https://speakerdeck.com/patw0929/shi-yong-jest-jin-xing-front-end-unit-test-xian-shang-react-du-shu-hui-ban) - [2017-07-20 [vue]如何為vue補上單元測試來確保品質(vue單元測試系列-1)](https://dotblogs.com.tw/kinanson/2017/07/20/075338) - [探討單元測試和整合測試的涵蓋範圍](https://ithelp.ithome.com.tw/articles/10229734) - [Defining Test Boundaries – An example](https://www.simpleorientedarchitecture.com/defining-test-boundaries/) - [UnitTest](https://martinfowler.com/bliki/UnitTest.html) ## 😂有趣的討論 - [[懶人包]DHH: TDD is dead. Long live testing. 懶人包整理](https://dotblogs.com.tw/hatelove/2014/05/03/relative-articles-of-dhh-tdd-is-dead) ## 💯技術文件 - [Vue测试指南](https://lmiller1990.github.io/vue-testing-handbook/zh-CN/#%E8%BF%99%E6%9C%AC%E6%8C%87%E5%8D%97%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Google Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully