天天看點

去中心化網絡的測試庫

去中心化網絡的測試庫

去中心化網絡應用的世界是一個令人興奮的地方,近年來已經爆炸性增長,IPFS和以太坊等技術為點對點的網絡提供了可能性,創造出生活在傳統的客戶/伺服器模式之外的應用,使用者可以直接互動和控制自己的資料。

同時,它仍然不成熟,對于軟體開發人員來說,它缺乏傳統的基于HTTP的網絡應用程式世界的許多能力和生态系統。在這個領域工作的開發者的工具和庫要少得多。

在過去的一年裡,我一直在努力改善這個問題(作為歐盟地平線的下一代網際網路計劃資助的項目的一部分),為IPFS和以太坊建立網絡攔截庫:MockIPFS和Mockthereum。這些庫既是一個立即有用的自動化測試庫,以支援現代內建測試和CI工作流程,也是為使用任一(或兩個)技術的網絡應用程式建立更通用的網絡代理工具的基礎。

如果這聽起來很酷,而且你隻是想直接進入并親自嘗試這些,你可以從github.com/httptoolkit/mockipfs/和github.com/httptoolkit/mockthereum/開始。

另一方面,如果你想聽聽這在實踐中能做什麼,并了解一下它在引擎蓋下是如何工作的,請繼續閱讀。

建構網絡應用的新方法

去中心化的網絡應用程式通常使用許多不同的技術,在堆棧的不同層次上,如:

  • IPFS:用于去中心化的靜态内容托管和資料存儲
  • 以太坊:用于去中心化的全局狀态、對該狀态的計算和金融交易
  • Filecoin/Storj :用于付費的去中心化長期内容存儲
  • WebRTC:用于點對點的原始資料傳輸和視訊/音頻連接配接
  • Service workers:一個允許完全離線的網絡應用的JavaScript API
  • Handshake(HNS)/以太坊名稱系統(ENS):将域名映射到網絡應用上
  • GunDB:用于網絡的去中心化資料庫,具有點對點同步功能
  • HTTP:用于與現有的 “傳統”網絡互動,以及與允許通路許多這些協定的節點通信。

通過結合這些技術,有可能建立一個從分布式網絡提供服務的網絡應用程式,而不是一個可能離線或被封鎖的單一伺服器,它可以存儲資料,與他人通信,并普遍提供你期望從傳統SaaS網絡應用程式獲得的所有功能。

現在,這種架構的一個例子看起來像:

  • 将一個基于JS的單頁網絡應用程式釋出到IPFS,使用服務工作者使其完全離線和本地運作
  • 使用HNS/ENS将域名映射到釋出的内容哈希上
  • 允許使用者通過WebRTC進行點對點的通信,直接發送消息或在上面使用GunDB來同步結構化資料存儲
  • 将使用者的持久性内容釋出到IPFS(可能是加密的),他們可以在自己的IPFS節點中固定,或通過Filecoin/Storj付費鏡像
  • 修改全局狀态或通過以太坊支援付費交易。

鑒于這樣的設定,擁有相容浏覽器的使用者(預設為Brave,或安裝了IPFS伴侶和Metamask擴充的Chrome/Firefox等)可以加載網絡應用,在他們的機器上使用它,并從其他人那裡發送和接收資料,所有這些都沒有一個中央伺服器參與,所有資料都存儲在本地,或在他們自己控制的服務中。

即使原來的出版商不存在了,他們所有的基礎設施都關閉了,如果圍繞這個模型設計得好,使用者将能夠永遠使用這個應用程式。

這至少是理論上的。在實踐中,有相當多的粗糙邊緣,是以這是很複雜和具有挑戰性的,但這是一個有趣的空間,有許多新技術不斷出現和發展。即使在今天,上面的清單也遠遠沒有完成,這些技術放在一起,暗示了網絡上去中心化技術的一個有趣的未來。

不過,HTTP如何與此相聯系是值得注意的。雖然這些協定中的每一個都是獨立于HTTP的,但對于網絡應用中的浏覽器連接配接,它們中的許多都使用HTTP作為最後一英裡的傳輸。例如,對于IPFS,你通常會在你的機器上運作一個IPFS節點,直接與IPFS網絡進行通信,然後配置你的浏覽器使用該節點進行所有的IPFS,然後所有的IPFS互動将通過從你的網絡應用中向該節點發出HTTP請求而發生。同樣,對于以太坊來說,在絕大多數情況下,網絡上的以太坊互動涉及到對托管的以太坊API的HTTP請求(這與中心化服務不一樣,因為任何工作節點都可以同樣工作,但必須使用一些托管節點)。

進入MockIPFS和Mockthereum

如果你建立一個這樣的網絡應用,你很快就會發現,測試它是一個嚴重的挑戰。幾乎沒有可用的工具或庫,是以你被迫要麼完全手動模拟出API、庫或原始HTTP請求(非難事,而且很難做到準确),要麼運作一個真正的IPFS/以太坊節點進行測試(慢、重、有限,而且有持久的狀态,有用,但不是你想要的自動化測試用例)。

MockIPFS和Mockthereum采取了不同的方法:在HTTP層面上的無狀态和完全可配置的模拟,對用戶端庫和托管節點之間使用的HTTP互動協定有内置的解釋和模拟。

這意味着你可以:

  • 在一行代碼中模拟兩種協定的最常見的互動結果。
  • 直接監控、記錄或斷言用戶端和網絡之間的所有以太坊/IPFS互動。
  • 模拟連接配接問題和逾時等場景。
  • 在幾毫秒内建立、重置和銷毀模拟節點。
  • 在同一台機器上同時運作多個完全隔離的模拟節點,以最小的開銷,輕松地并行運作測試。

用MockIPFS測試一個使用IPFS的dweb應用

一個去中心化的網絡應用有很多方式想與IPFS互動,但最常見的是你想從一個CID中讀取一些IPFS資料,是以讓我們用這個作為例子。

要在網絡上做到這一點,你通常要寫這樣的代碼:

import * as IPFS from "ipfs-http-client";
import itAll from 'it-all';
import {
    concat as uint8ArrayConcat,
    toString as uint8ToString
} from 'uint8arrays';

const IPFS_CONTENT_PATH = '/ipfs/Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu';

async function runMyApp(ipfsNodeConfig) {
    const ipfsClient = IPFS.create(ipfsNodeConfig);

    // ...
    // Somewhere in your code, read some content from IPFS:
    const content = await itAll(ipfs.cat(IPFS_CONTENT_PATH));
    const contentText = uint8ToString(uint8ArrayConcat(content));
    // ...
}

runMyApp({ /* Your IPFS node config */ });           

這使用了ipfs-http-client,這是一個廣泛使用的官方庫,用于在網絡上使用IPFS,向本地IPFS節點發出HTTP請求,以擷取IPFS的内容ID(在這個例子中為Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu)。

使用MockIPFS來測試這段代碼,并模拟出傳回的結果,看起來是這樣的:

// Import MockIPFS and create a fake node:
import * as MockIPFS from 'mockipfs';
const mockNode = MockIPFS.getLocal();

describe("Your tests", () => {
    // Start & stop your mock node to reset state between tests
    beforeEach(() => mockNode.start());
    afterEach(() => mockNode.stop());

    it("can mock & query IPFS interactions", async () => {
        // Define a rule to mock out this content:
        const ipfsPath = "/ipfs/Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu";
        const mockedContent = await mockNode.forCat(ipfsPath).thenReturn("Mock content");

        // Run the code that you want to test, configuring the app to use your mock node:
        await runMyApp(mockNode.ipfsOptions); // <-- IPFS cat() here will read 'Mock content'

        // Afterwards, assert that we saw the requests we expected:
        const catRequests = await mockNode.getQueriedContent();
        expect(catRequests).to.deep.equal([
            { path: ipfsPath }
        ]);
    });
});           

在這種情況下,MockIPFS處理請求,解析API調用以比對所使用的特定CID,然後傳回正确程式設計和格式的内容,就像一個真正的IPFS節點,完全內建測試你的應用程式的整個用戶端代碼,但沒有真正的IPFS節點的開銷、複雜性或不可預測性。

像這樣模拟ipfs.cat是最簡單的情況,但MockIPFS可以走得更遠:

  • 通過mockNode.forPinAdd(cid).... 等調用,測試内容引腳和取消引腳,例如對無效/重複的引腳抛出錯誤。
  • 用mockNode.forNameResolve(name).thenTimeout()為IPNS查詢注入逾時。
  • 用mockNode.forAdd().thenAcceptPublishAs(hash)模拟内容釋出結果。

要想開始,請看README以獲得更多的細節和完整的API文檔,或者看一下測試套件以獲得涵蓋IPFS API每個主要領域的完整工作執行個體。

使用Mockthereum測試以太坊的dweb應用

在以太坊上建構網絡應用時,一個常見的互動是調用合約--即查詢區塊鍊上的資料,而不實際建立交易。

使用流行的以太坊網絡用戶端Web3.js,這樣做的代碼可能看起來像:

import Web3 from 'web3';

// Parameters for some real Web3 contract:
const CONTRACT_ADDRESS = "0x...";
const JSON_CONTRACT_ABI = { /* ... */ };

async function runMyApp(ethNodeAddress) {
    const web3 = new Web3(ethNodeAddress);

    // ...
    // Somewhere in your code, call a method on the Ethereum contract:
    const contract = new web3.eth.Contract(JSON_CONTRACT_ABI, CONTRACT_ADDRESS);
    const contractResult = await contract.methods.getText("test").call();
    // ...
}

runMyApp(/* Your Ethereum node API address */);           

和上面的IPFS一樣,我們可以很容易地定義一個模拟節點,它可以攔截這個請求,傳回任何值或者模拟任何你想要的其他行為:

// Import Mockthere and create a fake node:
import * as Mockthereum from 'mockthereum';
const mockNode = Mockthereum.getLocal();

describe("Your tests", () => {
    // Start & stop your mock node to reset state between tests
    beforeEach(() => mockNode.start());
    afterEach(() => mockNode.stop());

    it("can mock & query Ethereum interactions", async () => {
        // Define a rule to mock out the specific contract method that's called:
        const mockedFunction = await mockNode.forCall(CONTRACT_ADDRESS) // Match any contract address
            // Optionally, match specific functions and parameters:
            .forFunction('function getText(string key) returns (string)')
            .withParams(["test"])
            // Mock contract results:
            .thenReturn('Mock result');

        // Run the code that you want to test, configuring the app to use your mock node:
        await runMyApp(mockNode.url); // <-- Contract call here will read 'Mock result'

        // Afterwards, assert that we saw the contrat calls we expected:
        const mockedCalls = await mockedFunction.getRequests();
        expect(mockedCalls.length).to.equal(1);

        expect(mockedCalls[0]).to.deep.include({
            // Examine full interaction data, included decoded parameters etc:
            to: CONTRACT_ADDRESS,
            params: ["test"]
        });
    });
});           

要想開始了解許多其他可以模拟的以太坊行為,請看README,或者看一下測試套件中涵蓋廣泛的典型以太坊互動的完整工作執行個體。

測試之外

在上面的快速例子中,我們已經看到了MockIPFS和Mockthereum如何處理特定的普通互動的簡單示範,通過配置用戶端的模拟節點位址而不是真實節點,這樣模拟節點就可以獨立于更廣泛的網絡處理所有流量。

當這樣使用時,所有不比對的請求将收到預設響應,例如,所有IPFS添加請求将顯示為成功(同時沒有真正釋出任何東西),所有以太坊錢包餘額将為零。

不過這兩個庫都可以超越這一點。每個庫都可以配置為将不比對的請求轉發到其他地方,這樣部分或全部流量就會通過模拟節點傳遞到真正的IPFS/以太坊節點。這使得它有可能為調試而記錄流量,或者隻模拟互動的一個子集,而所有其他的請求都表現得很正常。

要配置這一點,在建立模拟節點時,在getLocal調用中傳遞一個unmatchedRequests選項,像這樣:

const ipfsMockNode = MockIPFS.getLocal({
  unmatchedRequests: { proxyTo: "http://localhost:5001" }
});
const ethMockNode = Mockthereum.getLocal({
    unmatchedRequests: { proxyTo: "http://localhost:30303" }
});           

通過這種配置,你可以在浏覽器中把這些節點作為你的正常節點位址(通過在IPFS companion/Metamask/等中配置位址),用于進階代理用例。預設情況下,它們的行為就像它們代理的真實節點一樣,但你可以額外添加接收到的互動的日志,以便在你浏覽網頁時監控用戶端以太坊/IPFS的互動,或者你可以通過添加規則來比對這些請求,模拟出甚至禁用某些類型的互動。

為自己入門

在保持本文簡短的同時,很難将這些工具的所有功能都擠進去。但如果這已經激起了你的興趣,可以去GitHub上看看這些庫本身,看看深入的入門指南和解釋,以及涵蓋其全部功能的詳細API文檔:MockIPFS, Mockthereum.

繼續閱讀