zoukankan      html  css  js  c++  java
  • 轻松编写 C++ 单元测试

    单元测试概述

    测试并不只是测试工程师的责任,对于开发工程师,为了保证发布给测试环节的代码具有足够好的质量( Quality ),为所编写的功能代码编写适量的单元测试是十分必要的。

    单元测试( Unit Test ,模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确,通过编写单元测试可以在编码阶段发现程序编码错误,甚至是程序设计错误。

    单元测试不但可以增加开发者对于所完成代码的自信,同时,好的单元测试用例往往可以在回归测试的过程中,很好地保证之前所发生的修改没有破坏已有的程序逻辑。因此,单元测试不但不会成为开发者的负担,反而可以在保证开发质量的情况下,加速迭代开发的过程。

    对于单元测试框架,目前最为大家所熟知的是 JUnit 及其针对各语言的衍生产品, C++ 语言所对应的 JUnit 系单元测试框架就是 CppUnit 。但是由于 CppUnit 的设计严格继承自 JUnit ,而没有充分考虑 C++ 与 Java 固有的差异(主要是由于 C++ 没有反射机制,而这是 JUnit 设计的基础),在 C++ 中使用 CppUnit 进行单元测试显得十分繁琐,这一定程度上制约了 CppUnit 的普及。笔者在这里要跟大家介绍的是一套由 google 发布的开源单元测试框架( Testing Framework ): googletest 。

    应用 googletest 编写单元测试代码

    googletest 是由 Google 公司发布,且遵循 New BSD License (可用作商业用途)的开源项目,并且 googletest 可以支持绝大多数大家所熟知的平台。与 CppUnit 不同的是: googletest 可以自动记录下所有定义好的测试,不需要用户通过列举来指明哪些测试需要运行。

    定义单元测试

    在应用 googletest 编写单元测试时,使用 TEST() 宏来声明测试函数。如:

    清单 1. 用 TEST() 宏声明测试函数
    1 TEST(GlobalConfigurationTest, configurationDataTest) 
    2 TEST(GlobalConfigurationTest, noConfigureFileTest)

    分别针对同一程序单元 GlobalConfiguration 声明了两个不同的测试(Test)函数,以分别对配置数据进行检查( configurationDataTest ),以及测试没有配置文件的特殊情况( noConfigureFileTest )。

    实现单元测试

    针对同一程序单元设计出不同的测试场景后(即划分出不同的 Test 后),开发者就可以编写单元测试分别实现这些测试场景了。

    在 googletest 中实现单元测试,可通过 ASSERT_* 和 EXPECT_* 断言来对程序运行结果进行检查。 ASSERT_* 版本的断言失败时会产生致命失败,并结束当前函数; EXPECT_* 版本的断言失败时产生非致命失败,但不会中止当前函数。因此, ASSERT_* 常常被用于后续测试逻辑强制依赖的处理结果的断言,如创建对象后检查指针是否为空,若为空,则后续对象方法调用会失败;而 EXPECT_* 则用于即使失败也不会影响后续测试逻辑的处理结果的断言,如某个方法返回结果的多个属性的检查。

    googletest 中定义了如下的断言:

    表 1: googletest 定义的断言( Assert )
    基本断言二进制比较字符串比较
    ASSERT_TRUE(condition);
    EXPECT_TRUE(condition);
    condition为真
    ASSERT_FALSE(condition);
    EXPECT_FALSE(condition);
    condition为假
    ASSERT_EQ(expected,actual);
    EXPECT_EQ(expected,actual);
    expected==actual
    ASSERT_NE(val1,val2);
    EXPECT_NE(val1,val2);
    val1!=val2
    ASSERT_LT(val1,val2);
    EXPECT_LT(val1,val2);
    val1<val2
    ASSERT_LE(val1,val2);
    EXPECT_LE(val1,val2);
    val1<=val2
    ASSERT_GT(val1,val2);
    EXPECT_GT(val1,val2);
    val1>val2
    ASSERT_GE(val1,val2);
    EXPECT_GE(val1,val2);
    val1>=val2
    ASSERT_STREQ(expected_str,actual_str);
    EXPECT_STREQ(expected_str,actual_str);
    两个 C 字符串有相同的内容
    ASSERT_STRNE(str1,str2);
    EXPECT_STRNE(str1,str2);
    两个 C 字符串有不同的内容
    ASSERT_STRCASEEQ(expected_str,actual_str);
    EXPECT_STRCASEEQ(expected_str,actual_str);
    两个 C 字符串有相同的内容,忽略大小写
    ASSERT_STRCASENE(str1,str2);
    EXPECT_STRCASENE(str1,str2);
    两个 C 字符串有不同的内容,忽略大小写

    下面的实例演示了上面部分断言的使用:

    清单 2. 一个较完整的 googletest 单元测试实例
     1 // Configure.h 
     2  #pragma once 
     3 
     4  #include <string> 
     5  #include <vector> 
     6 
     7  class Configure 
     8  { 
     9  private: 
    10     std::vector<std::string> vItems; 
    11 
    12  public: 
    13     int addItem(std::string str); 
    14 
    15     std::string getItem(int index); 
    16 
    17     int getSize(); 
    18  }; 
    19 
    20  // Configure.cpp 
    21  #include "Configure.h" 
    22 
    23  #include <algorithm> 
    24 
    25  /** 
    26  * @brief Add an item to configuration store. Duplicate item will be ignored 
    27  * @param str item to be stored 
    28  * @return the index of added configuration item 
    29  */ 
    30  int Configure::addItem(std::string str) 
    31  { 
    32 std::vector<std::string>::const_iterator vi=std::find(vItems.begin(), vItems.end(), str); 
    33     if (vi != vItems.end()) 
    34         return vi - vItems.begin(); 
    35 
    36     vItems.push_back(str); 
    37     return vItems.size() - 1; 
    38  } 
    39 
    40  /** 
    41  * @brief Return the configure item at specified index. 
    42  * If the index is out of range, "" will be returned 
    43  * @param index the index of item 
    44  * @return the item at specified index 
    45  */ 
    46  std::string Configure::getItem(int index) 
    47  { 
    48     if (index >= vItems.size()) 
    49         return ""; 
    50     else 
    51         return vItems.at(index); 
    52  } 
    53 
    54  /// Retrieve the information about how many configuration items we have had 
    55  int Configure::getSize() 
    56  { 
    57     return vItems.size(); 
    58  } 
    59 
    60  // ConfigureTest.cpp 
    61  #include <gtest/gtest.h> 
    62 
    63  #include "Configure.h" 
    64 
    65  TEST(ConfigureTest, addItem) 
    66  { 
    67     // do some initialization 
    68     Configure* pc = new Configure(); 
    69     
    70     // validate the pointer is not null 
    71     ASSERT_TRUE(pc != NULL); 
    72 
    73     // call the method we want to test 
    74     pc->addItem("A"); 
    75     pc->addItem("B"); 
    76     pc->addItem("A"); 
    77 
    78     // validate the result after operation 
    79     EXPECT_EQ(pc->getSize(), 2); 
    80     EXPECT_STREQ(pc->getItem(0).c_str(), "A"); 
    81     EXPECT_STREQ(pc->getItem(1).c_str(), "B"); 
    82     EXPECT_STREQ(pc->getItem(10).c_str(), ""); 
    83 
    84     delete pc; 
    85  }

    运行单元测试

    在实现完单元测试的测试逻辑后,可以通过 RUN_ALL_TESTS() 来运行它们,如果所有测试成功,该函数返回 0,否则会返回 1 。 RUN_ALL_TESTS() 会运行你链接到的所有测试――它们可以来自不同的测试案例,甚至是来自不同的文件。

    因此,运行 googletest 编写的单元测试的一种比较简单可行的方法是:

    • 为每一个被测试的 class 分别创建一个测试文件,并在该文件中编写针对这一 class 的单元测试;
    • 编写一个 Main.cpp 文件,并在其中包含以下代码,以运行所有单元测试:
    清单 3. 初始化 googletest 并运行所有测试
    1 #include <gtest/gtest.h> 
    2 
    3  int main(int argc, char** argv) { 
    4     testing::InitGoogleTest(&argc, argv); 
    5 
    6     // Runs all tests using Google Test. 
    7     return RUN_ALL_TESTS(); 
    8  }
    • 最后,将所有测试代码及 Main.cpp 编译并链接到目标程序中。

    此外,在运行可执行目标程序时,可以使用 --gtest_filter 来指定要执行的测试用例,如:

    • ./foo_test 没有指定filter,运行所有测试;
    • ./foo_test --gtest_filter=* 指定filter*,运行所有测试;
    • ./foo_test --gtest_filter=FooTest.* 运行测试用例FooTest的所有测试;
    • ./foo_test --gtest_filter=*Null*:*Constructor* 运行所有全名(即测试用例名 + “ . ” + 测试名,如 GlobalConfigurationTest.noConfigureFileTest含有"Null""Constructor"的测试;
    • ./foo_test --gtest_filter=FooTest.*-FooTest.Bar 运行测试用例FooTest的所有测试,但不包括FooTest.Bar

    这一特性在包含大量测试用例的项目中会十分有用。

     

    应用 googlemock 编写 Mock Objects

    很多 C++ 程序员对于 Mock Objects (模拟对象)可能比较陌生,模拟对象主要用于模拟整个应用程序的一部分。在单元测试用例编写过程中,常常需要编写模拟对象来隔离被测试单元的“下游”或“上游”程序逻辑或环境,从而达到对需要测试的部分进行隔离测试的目的。

    例如,要对一个使用数据库的对象进行单元测试,安装、配置、启动数据库、运行测试,然后再卸装数据库的方式,不但很麻烦,过于耗时,而且容易由于环境因素造成测试失败,达不到单元测试的目的。模仿对象提供了解决这一问题的方法:模仿对象符合实际对象的接口,但只包含用来“欺骗”测试对象并跟踪其行为的必要代码。因此,其实现往往比实际实现类简单很多。

    为了配合单元测试中对 Mocking Framework 的需要, Google 开发并于 2008 年底开放了: googlemock 。与 googletest 一样, googlemock 也是遵循 New BSD License (可用作商业用途)的开源项目,并且 googlemock 也可以支持绝大多数大家所熟知的平台。

    注 1:在 Windows 平台上编译 googlemock

    对于 Linux 平台开发者而言,编译 googlemock 可能不会遇到什么麻烦;但是对于 Windows 平台的开发者,由于 Visual Studio 还没有提供 tuple ( C++0x TR1 中新增的数据类型)的实现,编译 googlemock 需要为其指定一个 tuple 类型的实现。著名的开源 C++ 程序库 boost 已经提供了 tr1 的实现,因此,在 Windows 平台下可以使用 boost 来编译 googlemock 。为此,需要修改 %GMOCK_DIR%/msvc/gmock_config.vsprops ,设定其中 BoostDir 到 boost 所在的目录,如:

    1 <UserMacro 
    2     Name="BoostDir" 
    3     Value="$(BOOST_DIR)" 
    4  />

    其中 BOOST_DIR 是一个环境变量,其值为 boost 库解压后所在目录。

    对于不希望在自己的开发环境上解包 boost 库的开发者,在 googlemock 的网站上还提供了一个从 boost 库中单独提取出来的 tr1 的实现,可将其下载后将解压目录下的 boost 目录拷贝到 %GMOCK_DIR% 下(这种情况下,请勿修改上面的配置项;建议对 boost 不甚了解的开发者采用后面这种方式)。

    在应用 googlemock 来编写 Mock 类辅助单元测试时,需要:

    • 编写一个 Mock Class (如 class MockTurtle ),派生自待 Mock 的抽象类(如 class Turtle );
    • 对于原抽象类中各待 Mock 的 virtual 方法,计算出其参数个数 n ;
    • 在 Mock Class 类中,使用 MOCK_METHODn() (对于 const 方法则需用 MOCK_CONST_METHODn() )宏来声明相应的 Mock 方法,其中第一个参数为待 Mock 方法的方法名,第二个参数为待 Mock 方法的类型。如下:
    清单 4. 使用 MOCK_METHODn 声明 Mock 方法
     1 #include <gmock/gmock.h>  // Brings in Google Mock. 
     2 
     3  class MockTurtle : public Turtle { 
     4     MOCK_METHOD0(PenUp, void()); 
     5     MOCK_METHOD0(PenDown, void()); 
     6     MOCK_METHOD1(Forward, void(int distance)); 
     7     MOCK_METHOD1(Turn, void(int degrees)); 
     8     MOCK_METHOD2(GoTo, void(int x, int y)); 
     9     MOCK_CONST_METHOD0(GetX, int()); 
    10     MOCK_CONST_METHOD0(GetY, int()); 
    11  };
    • 在完成上述工作后,就可以开始编写相应的单元测试用例了。在编写单元测试时,可通过 ON_CALL 宏来指定 Mock 方法被调用时的行为,或 EXPECT_CALL 宏来指定 Mock 方法被调用的次数、被调用时需执行的操作等,并对执行结果进行检查。如下:
    清单 5. 使用 ON_CALL 及 EXPECT_CALL 宏
     1 using testing::Return;                              // #1,必要的声明
     2 
     3  TEST(BarTest, DoesThis) { 
     4     MockFoo foo;                                    // #2,创建 Mock 对象
     5 
     6     ON_CALL(foo, GetSize())                         // #3,设定 Mock 对象默认的行为(可选)
     7         .WillByDefault(Return(1)); 
     8     // ... other default actions ... 
     9 
    10     EXPECT_CALL(foo, Describe(5))                   // #4,设定期望对象被访问的方式及其响应
    11         .Times(3) 
    12         .WillRepeatedly(Return("Category 5")); 
    13     // ... other expectations ... 
    14 
    15     EXPECT_EQ("good", MyProductionFunction(&foo));  
    16     // #5,操作 Mock 对象并使用 googletest 提供的断言验证处理结果
    17  }                                                  
    18  // #6,当 Mock 对象被析构时, googlemock 会对结果进行验证以判断其行为是否与所有设定的预期一致

    其中, WillByDefault 用于指定 Mock 方法被调用时的默认行为; Return 用于指定方法被调用时的返回值; Times 用于指定方法被调用的次数; WillRepeatedly 用于指定方法被调用时重复的行为。

    对于未通过 EXPECT_CALL 声明而被调用的方法,或不满足 EXPECT_CALL 设定条件的 Mock 方法调用, googlemock 会输出警告信息。对于前一种情况下的警告信息,如果开发者并不关心这些信息,可以使用 Adapter 类模板 NiceMock 避免收到这一类警告信息。如下:

    清单 6. 使用 NiceMock 模板
    1 testing::NiceMock<MockFoo> nice_foo;

    在笔者开发的应用中,被测试单元会通过初始化时传入的上层应用的接口指针,产生大量的处理成功或者失败的消息给上层应用,而开发者在编写单元测试时并不关心这些消息的内容,通过使用 NiceMock 可以避免为不关心的方法编写 Mock 代码(注意:这些方法仍需在 Mock 类中声明,否则 Mock 类会被当作 abstract class 而无法实例化)。

    与 googletest 一样,在编写完单元测试后,也需要编写一个如下的入口函数来执行所有的测试:

    清单 7. 初始化 googlemock 并运行所有测试
    1 #include <gtest/gtest.h> 
    2  #include <gmock/gmock.h> 
    3 
    4  int main(int argc, char** argv) { 
    5     testing::InitGoogleMock(&argc, argv); 
    6 
    7     // Runs all tests using Google Test. 
    8     return RUN_ALL_TESTS(); 
    9  }

    下面的代码演示了如何使用 googlemock 来创建 Mock Objects 并设定其行为,从而达到对核心类 AccountService 的 transfer (转账)方法进行单元测试的目的。由于 AccountManager 类的具体实现涉及数据库等复杂的外部环境,不便直接使用,因此,在编写单元测试时,我们用 MockAccountManager 替换了具体的 AccountManager 实现。

    清单 8. 待测试的程序逻辑
      1 // Account.h 
      2  // basic application data class 
      3  #pragma once 
      4 
      5  #include <string> 
      6 
      7  class Account 
      8  { 
      9  private: 
     10     std::string accountId; 
     11 
     12     long balance; 
     13 
     14  public: 
     15     Account(); 
     16 
     17     Account(const std::string& accountId, long initialBalance); 
     18 
     19     void debit(long amount); 
     20 
     21     void credit(long amount); 
     22 
     23     long getBalance() const; 
     24 
     25     std::string getAccountId() const; 
     26  }; 
     27 
     28  // Account.cpp 
     29  #include "Account.h" 
     30 
     31  Account::Account() 
     32  { 
     33  } 
     34 
     35  Account::Account(const std::string& accountId, long initialBalance) 
     36  { 
     37     this->accountId = accountId; 
     38     this->balance = initialBalance; 
     39  } 
     40 
     41  void Account::debit(long amount) 
     42  { 
     43     this->balance -= amount; 
     44  } 
     45 
     46  void Account::credit(long amount) 
     47  { 
     48     this->balance += amount; 
     49  } 
     50 
     51  long Account::getBalance() const 
     52  { 
     53     return this->balance; 
     54  } 
     55 
     56  std::string Account::getAccountId() const 
     57  { 
     58     return accountId; 
     59  } 
     60 
     61  // AccountManager.h 
     62  // the interface of external services which should be mocked 
     63  #pragma once 
     64 
     65  #include <string> 
     66 
     67  #include "Account.h" 
     68 
     69  class AccountManager 
     70  { 
     71  public: 
     72     virtual Account findAccountForUser(const std::string& userId) = 0; 
     73 
     74     virtual void updateAccount(const Account& account) = 0; 
     75  }; 
     76 
     77  // AccountService.h 
     78  // the class to be tested 
     79  #pragma once 
     80 
     81  #include <string> 
     82 
     83  #include "Account.h" 
     84  #include "AccountManager.h" 
     85 
     86  class AccountService 
     87  { 
     88  private: 
     89     AccountManager* pAccountManager; 
     90 
     91  public: 
     92     AccountService(); 
     93 
     94     void setAccountManager(AccountManager* pManager); 
     95     void transfer(const std::string& senderId, 
     96                const std::string& beneficiaryId, long amount); 
     97  }; 
     98 
     99  // AccountService.cpp 
    100  #include "AccountService.h" 
    101 
    102  AccountService::AccountService() 
    103  { 
    104     this->pAccountManager = NULL; 
    105  } 
    106 
    107  void AccountService::setAccountManager(AccountManager* pManager) 
    108  { 
    109     this->pAccountManager = pManager; 
    110  } 
    111 
    112  void AccountService::transfer(const std::string& senderId, 
    113                   const std::string& beneficiaryId, long amount) 
    114  { 
    115     Account sender = this->pAccountManager->findAccountForUser(senderId); 
    116 
    117     Account beneficiary = this->pAccountManager->findAccountForUser(beneficiaryId); 
    118 
    119     sender.debit(amount); 
    120 
    121     beneficiary.credit(amount); 
    122 
    123     this->pAccountManager->updateAccount(sender); 
    124 
    125     this->pAccountManager->updateAccount(beneficiary); 
    126  }
    清单 9. 相应的单元测试
      1 // AccountServiceTest.cpp 
      2  // code to test AccountService 
      3  #include <map> 
      4  #include <string> 
      5 
      6  #include <gtest/gtest.h> 
      7  #include <gmock/gmock.h> 
      8 
      9  #include "../Account.h" 
     10  #include "../AccountService.h" 
     11  #include "../AccountManager.h" 
     12 
     13  // MockAccountManager, mock AccountManager with googlemock 
     14  class MockAccountManager : public AccountManager 
     15  { 
     16  public: 
     17     MOCK_METHOD1(findAccountForUser, Account(const std::string&)); 
     18 
     19     MOCK_METHOD1(updateAccount, void(const Account&)); 
     20  }; 
     21 
     22  // A facility class acts as an external DB 
     23  class AccountHelper 
     24  { 
     25  private: 
     26     std::map<std::string, Account> mAccount; 
     27              // an internal map to store all Accounts for test 
     28 
     29  public: 
     30     AccountHelper(std::map<std::string, Account>& mAccount); 
     31 
     32     void updateAccount(const Account& account); 
     33 
     34     Account findAccountForUser(const std::string& userId); 
     35  }; 
     36 
     37  AccountHelper::AccountHelper(std::map<std::string, Account>& mAccount) 
     38  { 
     39     this->mAccount = mAccount; 
     40  } 
     41 
     42  void AccountHelper::updateAccount(const Account& account) 
     43  { 
     44     this->mAccount[account.getAccountId()] = account; 
     45  } 
     46 
     47  Account AccountHelper::findAccountForUser(const std::string& userId) 
     48  { 
     49     if (this->mAccount.find(userId) != this->mAccount.end()) 
     50         return this->mAccount[userId]; 
     51     else 
     52         return Account(); 
     53  } 
     54 
     55  // Test case to test AccountService 
     56  TEST(AccountServiceTest, transferTest) 
     57  { 
     58     std::map<std::string, Account> mAccount; 
     59     mAccount["A"] = Account("A", 3000); 
     60     mAccount["B"] = Account("B", 2000); 
     61     AccountHelper helper(mAccount); 
     62 
     63     MockAccountManager* pManager = new MockAccountManager(); 
     64 
     65     // specify the behavior of MockAccountManager 
     66     // always invoke AccountHelper::findAccountForUser 
     67      // when AccountManager::findAccountForUser is invoked 
     68     EXPECT_CALL(*pManager, findAccountForUser(testing::_)).WillRepeatedly( 
     69         testing::Invoke(&helper, &AccountHelper::findAccountForUser)); 
     70 
     71     // always invoke AccountHelper::updateAccount 
     72     //when AccountManager::updateAccount is invoked 
     73     EXPECT_CALL(*pManager, updateAccount(testing::_)).WillRepeatedly( 
     74         testing::Invoke(&helper, &AccountHelper::updateAccount)); 
     75 
     76     AccountService as; 
     77     // inject the MockAccountManager object into AccountService 
     78     as.setAccountManager(pManager); 
     79 
     80     // operate AccountService 
     81     as.transfer("A", "B", 1005); 
     82 
     83     // check the balance of Account("A") and Account("B") to 
     84     //verify that AccountService has done the right job 
     85     EXPECT_EQ(1995, helper.findAccountForUser("A").getBalance()); 
     86     EXPECT_EQ(3005, helper.findAccountForUser("B").getBalance()); 
     87 
     88     delete pManager; 
     89  } 
     90 
     91  // Main.cpp 
     92  #include <gtest/gtest.h> 
     93  #include <gmock/gmock.h> 
     94 
     95  int main(int argc, char** argv) { 
     96     testing::InitGoogleMock(&argc, argv); 
     97 
     98     // Runs all tests using Google Test. 
     99     return RUN_ALL_TESTS(); 
    100  }

    注 2:上述范例工程详见附件。要编译该工程,请读者自行添加环境变量 GTEST_DIR 、 GMOCK_DIR ,分别指向 googletest 、 googlemock 解压后所在目录;对于 Windows 开发者,还需要将 %GMOCK_DIR%/msvc/gmock_config.vsprops 通过 View->Property Manager 添加到工程中,并将 gmock.lib 拷贝到工程目录下。

    通过上面的实例可以看出, googlemock 为开发者设定 Mock 类行为,跟踪程序运行过程及结果,提供了丰富的支持。但与此同时,应用程序也应该尽量降低应用代码间的耦合度,使得单元测试可以很容易对被测试单元进行隔离(如上例中, AccountService 必须提供了相应的方法以支持 AccountManager 的替换)。关于如何通过应用设计模式来降低应用代码间的耦合度,从而编写出易于单元测试的代码,请参考本人的另一篇文章《应用设计模式编写易于单元测试的代码》( developerWorks , 2008 年 7 月)。

    注 3:此外,开发者也可以直接通过继承被测试类,修改与外围环境相关的方法的实现,达到对其核心方法进行单元测试的目的。但由于这种方法直接改变了被测试类的行为,同时,对被测试类自身的结构有一些要求,因此,适用范围比较小,笔者也并不推荐采用这种原始的 Mock 方式来进行单元测试。

    总结

    Googletest 与 googlemock 的组合,很大程度上简化了开发者进行 C++ 应用程序单元测试的编码工作,使得单元测试对于 C++ 开发者也可以变得十分轻松;同时, googletest 及 googlemock 目前仍在不断改进中,相信随着其不断发展,这一 C++ 单元测试的全新组合将变得越来越成熟、越来越强大,也越来越易用。

    转自:https://www.ibm.com/developerworks/cn/linux/l-cn-cppunittest/

  • 相关阅读:
    jQuery 获取 select 值和文本
    js 获取字符串中最后一个斜杠后面的内容
    asp.net web.config 经典模式和集成模式相关配置
    IIS8 web.config 重定向之后 报错 500.19
    nodeJs学习-06 模块化、系统模块、自定义模块、express框架
    nodeJs学习-05 案例:http/fs/querystring/url
    nodeJs学习-04 POST数据请求,分段发送,分段接收
    nodeJs学习-03 GET数据请求,js拆解/querystring/url
    nodeJs学习-02 fs模块(文件操作)
    nodeJs学习-01 http模块
  • 原文地址:https://www.cnblogs.com/fnlingnzb-learner/p/6927834.html
Copyright © 2011-2022 走看看